Одного разу до нас прийшов стартап, який хотів впровадити анотування 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 редактора діаграм.







