Реалізація колаборативного малювання в мобільному додатку
Зустрічається ситуація: два користувачі малюють на одному полотні, і через 200 мс штрихи накладаються не в тому порядку. Причина — відсутність чесного впорядкування. Ми вирішуємо це за допомогою CRDT (Conflict-free Replicated Data Type). Наш стек — Flutter 3.x, Kotlin Multiplatform, Swift 5.9. Синхронізація побудована на бінарному протоколі поверх WebSocket, що дає затримку менше 100 мс при 60 FPS.
Бінарний протокол замість JSON дає виграш у 3 рази за об'ємом даних. Типова помилка — відправляти кожну подію окремо. Ми використовуємо батчинг: групуємо точки за 50–80 мс і відправляємо одним повідомленням. Це знижує навантаження на канал і сервер, економлячи ресурси.
Як працює CRDT для малювання?
Конфлікти при одночасному малюванні в одній області — часта проблема. Ми застосовуємо CRDT з вектором годинників. Кожна дія (початок, точки, завершення штриха) отримує мітку часу. При отриманні конфліктуючих правок порядок відновлюється за вектором: відправник не чекає підтвердження, а просто застосовує локально і фоново синхронізує. Це гарантує відсутність втрати даних і чесний порядок.
Як забезпечується локальний відгук при віддаленій синхронізації?
Користувач повинен бачити свій штрих одразу, без очікування мережі. Для цього ми рендеримо локальний штрих негайно, а синхронізацію запускаємо паралельно.
// Flutter: локальний штрих class DrawingBloc extends Bloc<DrawingEvent, DrawingState> { void onPointerDown(PointerDownEvent e) { currentStroke = Stroke(id: uuid(), points: [e.localPosition]); emit(state.copyWith(activeStroke: currentStroke)); _syncService.beginStroke(currentStroke.id, color, brushSize); } void onPointerMove(PointerMoveEvent e) { currentStroke.points.add(e.localPosition); emit(state.copyWith(activeStroke: currentStroke)); _syncService.appendPoints(currentStroke.id, [e.localPosition]); } void onPointerUp(PointerUpEvent e) { _syncService.finalizeStroke(currentStroke.id); } } Синхронізація йде незалежно — локальний рендер не блокується.
Чому важливий батчинг точок?
На мобільному пристрої 60 кадрів на секунду генерують до 60 подій pointerMove. Якщо відправляти кожну одразу, буде надлишковий трафік і затримка на серіалізацію. Ми збираємо точки в батчі по 10–15 штук і відправляємо кожні 50–80 мс. Це баланс між затримкою і трафіком.
Формат бінарного повідомлення (економія 3–5x порівняно з JSON):
[strokeId: 16 bytes][pointCount: uint8][x1:f32][y1:f32][p1:f16][x2:f32]... Float16 для тиску (0.0–1.0 з точністю 0.001) достатньо. Координати — float32 для субпіксельної точності. Разом ~10 байт на точку проти ~30 в JSON.
На WebSocket передаємо бінарні кадри (ArrayBuffer в JS, Uint8List в Dart, ByteBuffer в Kotlin).
Алгоритм згладжування на стороні отримувача
Віддалені точки приходять батчами з затримкою і дискретно. Проста відмальовка ліній між точками дає східчасті лінії. Потрібне згладжування. Ми рекомендуємо два методи:
- Catmull-Rom Spline — проходить через всі контрольні точки, низька складність, середня якість.
- Perfect Freehand — симулює форму пензля з урахуванням тиску і швидкості, генерує SVG-path. Висока якість, середня складність.
Для віддаленого штриха застосовуємо Perfect Freehand до всього масиву точок при кожному оновленні. Path перемальовується повністю — це дорожче по CPU, але візуально правильно (згладжування враховує весь контекст).
| Метод згладжування | Складність | Якість |
|---|---|---|
| Catmull-Rom Spline | Низька | Середня |
| Perfect Freehand | Середня | Висока |
Шари і порядок об'єктів
Базова модель: кожен штрих має z-index (timestamp створення). При конкурентному малюванні в одній області — хто намалював останнім, той зверху.
Шари (layers) — опціональна фіча. Кожен шар — окремий Y.Array об'єктів. Користувач вибирає активний шар. Видимість/блокування шару — поле в Y.Map шару.
При рендерингу: Canvas малює шари знизу вгору. Кожен шар — окремий offscreen canvas (iOS: UIGraphicsImageRenderer, Android: Bitmap з Canvas). Шари кешуються і перемальовуються тільки при зміні.
Ластик: спеціальний інструмент
Ластик не малює білим — він видаляє пікселі. Два варіанти:
- Object-level eraser — видаляє весь штрих при перетині. Простий у реалізації, добре синхронізується (delete(strokeId) — атомарна операція).
- Pixel-level eraser — розрізає штрих на частини. Вимагає геометричних обчислень (clip polygon by path). У колаборативному контексті складніше — потрібно розрізати штрих і створити нові об'єкти, що проблема для CRDT.
Ми рекомендуємо object-level eraser для початку.
Apple Pencil та Android Stylus
Apple Pencil через UITouch.type == .pencil передає тиск (force 0.0–1.0), кут нахилу (altitudeAngle, azimuthAngle) та передбачені точки (predictedTouches). Flutter уніфікує: PointerEvent.pressure, PointerEvent.tilt, PointerEvent.orientation. Синхронізація pressure/tilt на віддалені клієнти дає реалістичне відображення пензля партнера.
Як інтегрувати колаборативне малювання: покрокова інструкція
- Аналіз вимог — визначте кількість одночасно малюючих, необхідні інструменти, рівень згладжування.
- Вибір стеку — Flutter, Kotlin Multiplatform або нативний Swift/Kotlin. Ми рекомендуємо Flutter для кроссплатформенності.
- Інтеграція SDK — підключіть наш SDK, дотримуйтесь документації. Приклади коду для iOS, Android та Flutter включені.
- Налаштування серверної частини — розгорніть WebSocket-сервер з підтримкою CRDT. Ми надаємо Docker-образ.
- Тестування — перевірте синхронізацію при різних мережевих умовах, використовуйте TestFlight/Firebase Distribution.
- Деплой — публікація в App Store / Google Play.
Терміни та що входить
Колаборативне малювання з базовими інструментами (пензель, ластик, колір) на Flutter — 8–14 тижнів. З підтримкою стилуса, шарами, pixel-level eraser та масштабованим полотном — 20–28 тижнів.
У розробку входить:
- Документація API та архітектури
- Доступ до репозиторію з прикладами
- Навчання команди замовника
- Підтримка протягом місяця після релізу
Для точної оцінки вашого сценарію зв'яжіться з нами — ми розрахуємо терміни та бюджет під конкретні вимоги. Отримайте консультацію наших інженерів з 10-річним досвідом.
Типові помилки при реалізації:
- Відправка кожної події окремим повідомленням замість батчингу
- Використання JSON для точок — високий overhead
- Ігнорування передбачення дотиків для стилуса
- Відсутність CRDT для конфліктів
- Рендеринг всіх шарів кожен фрейм без кешування
| Транспорт | Розмір на точку | Затримка серіалізації |
|---|---|---|
| JSON | ~30 байт | ~0.5 мс |
| Бінарний | ~10 байт | ~0.1 мс |
Зв'яжіться з нами, щоб обговорити вашу задачу. Отримайте оцінку проекту вже сьогодні.







