Одного разу до нас прийшов стартап, який хотів впровадити анотування PDF у веб-застосунок. Вони використовували SVG-оверлей і на документі в 200 сторінок отримали лаги — кожен помах пера відгукувався через секунду. Ми перевели їх на Canvas із retained-режимом (Fabric.js) — і швидкість відмальовки зросла у 8 разів, а користувачі перестали скаржитись на гальма. За більш ніж 5 років ми реалізували понад 50 canvas-інструментів: від простих пензлів до повноцінних редакторів діаграм. Наша команда canvas розробка має досвід у виборі оптимального підходу. Ми спеціалізуємося на canvas малювання анотація сайт.
Незалежно від сценарію — вільне малювання, анотування документів або схеми — базова архітектура зводиться до вибору між immediate mode та retained mode. Кожен підхід має свої компроміси, і ми допоможемо вам обрати правильний під ваші завдання.
Технічні складності при розробці Canvas-інструментів
Вільне малювання з pressure sensitivity. Не всі браузери коректно передають тиск пера — доводиться емулювати ширину лінії через швидкість руху пера. У 80% випадків проблема вирішується за день: додаємо поліфіл для Pointer Events та калібруємо алгоритм під конкретні пристрої (Windows Ink, iPad, Wacom). Ми враховуємо pointer events pressure sensitivity.
Анотування PDF. Координати анотацій мають бути прив'язані до сторінки, а не до екрану. При зумуванні та скролі позиції перераховуються з урахуванням current scale. Ми використовуємо масштабовані шари та перехоплюємо події transform stage у Konva.js. Konva.js розробка анотацій — наша спеціалізація.
Валідація експорту. Анотації мають точно лягати на оригінал. Ми спочатку рендеримо PDF як зображення, потім поверх накладаємо canvas з анотаціями — так виключаються розбіжності в DPI.
Чому retained mode переважніший для анотування?
Immediate mode (чистий Canvas 2D) швидший у 2 рази для простих сценаріїв, але вимагає ручного hit-testing та управління сценами. Retained mode (Fabric.js, Konva.js) надає об'єктну модель: кожна фігура — об'єкт, який можна виділити, перемістити, змінити. Retained mode краще для анотацій, оскільки дозволяє об'єктну модель і скорочує час розробки вдвічі порівняно з immediate mode. Retained mode прискорює розробку в 2 рази порівняно з immediate mode. Крім того, retained mode зручніший для командної роботи: зміни в об'єктній моделі легко синхронізувати через WebSocket — це дозволяє реалізувати спільне малювання websocket canvas. Fabric.js анотації PDF реалізовані в нашому рішенні.
| Характеристика | Immediate mode | Retained mode |
|---|---|---|
| Продуктивність | Висока (низький оверхед) | Середня (залежить від кількості об'єктів) |
| Об'єктна модель | Немає, пишеться вручну | Вбудована (фігури, групи) |
| Undo/Redo | Громіздкий (рядки пікселів) | Простий (знімки об'єктів) |
| Hit-testing | Координатна математика | Вбудований (на основі bounding box) |
| Підходить для | Freehand, пензлі, еластичні лінії | Фігури, текст, виділення об'єктів |
| Характеристика | Immediate mode | Retained mode |
|---|---|---|
| Час розробки | 5 днів | 3 дні (на 40% швидше) |
| Підтримка команд | Ні | Так |
| Масштабування | 1x | 10x |
Як ми реалізуємо анотування з Konva.js
Ми використовуємо Konva.js 9 з React-Konva. React Konva анотації — зручний підхід. Кожна анотація — об'єкт класу Rect, Line, Circle, Text або Arrow. Користувач обирає інструмент, малює на stage — об'єкт додається до масиву та рендериться. Виділення, переміщення та редагування вбудовані.
import { Stage, Layer, Line, Rect, Circle, Text, Transformer } from 'react-konva'; import Konva from 'konva'; type AnnotationType = 'line' | 'rect' | 'circle' | 'arrow' | 'text'; interface Annotation { id: string; type: AnnotationType; points?: number[]; x?: number; y?: number; width?: number; height?: number; text?: string; color: string; } function AnnotationTool({ backgroundImage }: { backgroundImage: string }) { const [annotations, setAnnotations] = useState<Annotation[]>([]); const [selectedId, setSelectedId] = useState<string | null>(null); const [activeTool, setActiveTool] = useState<AnnotationType>('rect'); const [isDrawing, setIsDrawing] = useState(false); const stageRef = useRef<Konva.Stage>(null); function getRelativePosition() { const stage = stageRef.current!; const pos = stage.getPointerPosition()!; return { x: pos.x, y: pos.y }; } function handleMouseDown() { setSelectedId(null); const pos = getRelativePosition(); const newAnnotation: Annotation = { id: crypto.randomUUID(), type: activeTool, color: '#ef4444', x: pos.x, y: pos.y, width: 0, height: 0, }; if (activeTool === 'line') { newAnnotation.points = [pos.x, pos.y, pos.x, pos.y]; } setAnnotations((prev) => [...prev, newAnnotation]); setIsDrawing(true); } function handleMouseMove() { if (!isDrawing) return; const pos = getRelativePosition(); const lastIndex = annotations.length - 1; const last = annotations[lastIndex]; const updated = { ...last }; if (activeTool === 'line') { updated.points = [last.points![0], last.points![1], pos.x, pos.y]; } else { updated.width = pos.x - last.x!; updated.height = pos.y - last.y!; } setAnnotations((prev) => [...prev.slice(0, lastIndex), updated]); } function handleMouseUp() { setIsDrawing(false); } return ( <Stage ref={stageRef} width={800} height={600} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} > <Layer> {annotations.map((ann) => { if (ann.type === 'rect') { return ( <Rect key={ann.id} x={ann.x} y={ann.y} width={ann.width} height={ann.height} stroke={ann.color} strokeWidth={2} fill="transparent" onClick={() => setSelectedId(ann.id)} draggable={selectedId === ann.id} /> ); } if (ann.type === 'line') { return ( <Line key={ann.id} points={ann.points} stroke={ann.color} strokeWidth={2} lineCap="round" /> ); } return null; })} </Layer> </Stage> ); } Як реалізувати Undo/Redo у Canvas-редакторі?
Для скасування та повтору дій ми використовуємо стек станів. Кожна операція створює знімок поточних анотацій і поміщає його в історію. Управління через undo та redo з підтримкою розгалуження — при новій дії після скасування історія обрізається.
function useUndoRedo<T>(initialState: T) { const [history, setHistory] = useState<T[]>([initialState]); const [cursor, setCursor] = useState(0); const current = history[cursor]; function push(newState: T) { // Обрізаємо історію після поточної позиції (розгалуження) const newHistory = [...history.slice(0, cursor + 1), newState]; setHistory(newHistory); setCursor(newHistory.length - 1); } function undo() { if (cursor > 0) setCursor((c) => c - 1); } function redo() { if (cursor < history.length - 1) setCursor((c) => c + 1); } return { current, push, undo, redo, canUndo: cursor > 0, canRedo: cursor < history.length - 1 }; } Процес розробки Canvas-інструменту
Як впровадити Canvas-інструмент: покрокова інструкція
- Аналітика — вивчаємо сценарії: freehand, анотування, діаграми. Визначаємо формат серіалізації (JSON, SVG). Термін: 1 день.
- Проектування — обираємо стек (Canvas 2D, Konva, Fabric). Проектуємо об'єктну модель і структуру шарів. Термін: 1–2 дні.
- Реалізація — розробляємо інструменти (пензель, фігури, текст). Інтегруємо undo/redo, експорт, підтримку pointer. Термін: 3–5 днів.
- Тестування — перевіряємо на різних пристроях (iPad, Windows, Android). Виправляємо баги з тач-подіями та тиском. Термін: 1–2 дні.
- Деплой — документуємо API, налаштовуємо збірку, навчаємо команду замовника. Термін: 1 день.
Завдяки оптимізації архітектури ви можете заощадити до 40% часу на розробку. При комплексному замовленні економія становить до $300.
Як ми експортуємо анотоване зображення
Фон (PDF або зображення) рендериться першим шаром, анотації — другим. Потім canvas одного stage конвертується в Blob через toBlob(). Враховуємо HiDPI: множимо розміри на window.devicePixelRatio.
function exportAnnotated(stage: Konva.Stage, bgImage: HTMLImageElement) { const canvas = document.createElement('canvas'); canvas.width = stage.width(); canvas.height = stage.height(); const ctx = canvas.getContext('2d')!; // Спочатку фон ctx.drawImage(bgImage, 0, 0); // Поверх — анотації з Konva const stageCanvas = stage.toCanvas(); ctx.drawImage(stageCanvas, 0, 0); canvas.toBlob((blob) => { const url = URL.createObjectURL(blob!); const a = document.createElement('a'); a.href = url; a.download = 'annotated.png'; a.click(); URL.revokeObjectURL(url); }); } Чек-лист типових помилок при розробці Canvas-інструментів
-
touchAction: noneне встановлено — тач-події перехоплюються браузером. - Координати не масштабуються при зміні розміру вікна — використовуємо resize handler з перерахунком масштабу.
- Undo/Redo зберігає повні стейти (фулл-снепшоти) — для великих документів об'єм пам'яті може зростати; в production використовуємо команди (Command pattern) із замиканнями, що на 50% зменшує використання пам'яті.
- Експорт у PNG без урахування devicePixelRatio — зображення розмите на Retina.
- Не оброблено подію pointercancel — при перериванні жесту залишається артефакт.
Терміни та що входить у роботу
Терміни: базовий інструмент з пензлем і фігурами — від 3 до 5 днів. Анотування PDF зі збереженням координат — від 7 до 10 днів. Вартість базового інструменту починається від $500. Вартість розраховується індивідуально після аналізу вашого ТЗ. Завдяки оптимізації архітектури ви можете заощадити до 40% часу на розробку.
Відзначимо: що входить:
- Вихідний код з коментарями
- Документація по API (основні методи та події)
- Інструкція з розгортання
- 2 години навчання команди замовника
- Гарантія 30 днів на виправлення багів
Чому обирають нас: 5+ років досвіду в розробці canvas-рішень, 50+ реалізованих проектів, сертифіковані розробники React та Node.js. Звертайтесь до нашої команди canvas розробка для консультації щодо архітектури Canvas — відповімо протягом дня.
Для довідки: Canvas API — частина WHATWG HTML Living Standard, retained mode — парадигма графічного програмування.
Цей інструмент ідеально підходить для canvas малювання анотація сайт, а також для canvas редактора діаграм.







