Свой Miro за 30 минут вместо подписки
Как добавить свой live-whiteboard прямо в деке с синхро на iPad
Коротко: попросил Claude Code на Opus 4.6 положить прозрачный canvas поверх существующего React-дека, повесить на него PointerEvent с давлением, и поднять локальный WebSocket-broadcaster, который рассылает мазки между ноутбуком и iPad. Никаких SaaS, всё в локальной сети, четыре итерации до стабильной версии. Сегодня в 19:00 — лайв Урока 2 «Личной Корпорации», и мне нужно было рисовать прямо по слайдам с iPad, а не дёргаться в Excalidraw в соседнем табе.
Боль: alt-tab убивает поток
До этого я делал так: дек в браузере, Excalidraw во втором окне, на iPad — отдельная сессия Excalidraw через их облако. Когда хочется обвести фразу на слайде или дорисовать стрелку — alt-tab, рисуешь в пустоте, alt-tab обратно, объясняешь словами. Каждый раз режет поток.
Мне не нужен был «инструмент для рисования». Мне нужен был слой поверх моего дека, который включается одной клавишей и синхронится на iPad, чтобы я мог отойти от ноута и водить Pencil по экрану. Personal tool, software for one — ещё один personal-tool из этой серии.
Решение: два слоя + локальный broadcaster
Архитектура, которую я попросил собрать:
┌─────────────────────┐ ┌─────────────────────┐
│ MacBook M2 Pro │ │ iPad + Pencil │
│ │ │ │
│ ┌───────────────┐ │ │ ┌───────────────┐ │
│ │ Контент дека │ │ │ │ Контент дека │ │
│ │ (React SPA) │ │ │ │ (React SPA) │ │
│ └───────────────┘ │ │ └───────────────┘ │
│ ┌───────────────┐ │ │ ┌───────────────┐ │
│ │ Canvas-слой │ │ │ │ Canvas-слой │ │
│ │ (PointerEvent)│ │ │ │ (Apple Pencil)│ │
│ └───────┬───────┘ │ │ └───────┬───────┘ │
└──────────┼──────────┘ └──────────┼──────────┘
│ │
│ ws://192.168.1.x:5174 │
│ (LAN only) │
└───────────────┬───────────────┘
│
┌─────────▼─────────┐
│ WS-broadcaster │
│ - snapshot per │
│ slide │
│ - rebroadcast │
│ to others │
└───────────────────┘
Контент и canvas — два независимых слоя в одном React-дереве. Canvas прозрачный, активируется клавишей R или кнопкой-карандашом в правом верхнем углу (кнопка нужна, потому что у iPad клавиатуры нет). WebSocket-сервер живёт на том же ноуте, Vite поднят с host: true — iPad стучится по IP внутри сети.
Промпт, который я скинул
Скинул Claude Code на Opus 4.6:
Сделай мне презентационный дек как Vite + React SPA. Навигация — pill-bar с hash-routing, контент разбит на секции.
Поверх контента — слой для рисования:
- Активируется клавишей R и кнопкой-карандашом в правом верхнем углу (кнопка нужна для устройств без клавиатуры)
- Рисование через PointerEvent API с поддержкой Apple Pencil и давления
- Линии должны быть плавными — добавь Bezier-сглаживание по midpoints
- В режиме рисования текст под canvas не должен выделяться
Добавь WebSocket-сервер для live-sync рисунков по локальной сети:
- Новый клиент получает snapshot всего что уже нарисовано на текущем слайде
- Все события рисования рассылаются всем устройствам кроме отправителя
- Никаких внешних сервисов — только локальная сеть
- Vite должен быть доступен по IP в сети (host: true)
Отключи React StrictMode — он ломает WS при dev-режиме.
Через минуту у меня был работающий каркас: Vite + React + TypeScript, отдельный node-процесс с пакетом ws (v8.20) как broadcaster, два endpoint-а — snapshot для нового клиента и stroke для рассылки остальным. Дек я уже делал раньше похожим способом — тот же подход к презентациям без webpack, так что агент достроил поверх знакомой структуры.
Что попросил поправить за четыре итерации
Итерация 1 — сглаживание. Сырой PointerEvent даёт ломаную из точек. Линия выглядит как ЭКГ. Я написал агенту: «линии рваные, добавь сглаживание». Он переделал на quadratic Bezier по midpoints соседних точек — стандартный приём для HTML5 canvas Bezier smoothing, рисуется не от точки к точке, а от середины к середине через точку как control point. Apple Pencil sample rate 240 Hz, давление в PointerEvent.pressure от 0.0 до 1.0 (baseline в браузерах с июля 2020) — толщина мазка теперь зависит от нажима.
Итерация 2 — выделение текста. В режиме рисования pointer случайно цеплял текст под canvas, начинался drag-select. Агент добавил user-select: none на корневой контейнер при активном режиме и touch-action: none на сам canvas — без этого Safari на iPad сжирает события Pencil и пытается скроллить страницу.
Итерация 3 — кнопка для iPad. На ноуте всё хорошо: нажал R, рисуешь. На iPad клавиатуры нет, режим включить нечем. Попросил добавить плавающую кнопку-карандаш в правом верхнем углу — toggle между «pointer проходит сквозь canvas» и «pointer ловится canvas-ом». Через pointer-events: none на неактивном слое.
Итерация 4 — StrictMode ломает WebSocket. Коварный баг. React 18+ в dev-режиме монтирует компоненты дважды — это поведение StrictMode для отлова side-effects. Мой useEffect открывал WS-соединение, второй mount открывал ещё одно, первое закрывалось — broadcaster видел disconnect и сбрасывал клиент из списка. На iPad рисунки приходили рандомно. Я попросил отключить StrictMode в dev — не «правильный фикс», а компромисс для personal tool. В проде StrictMode no-op, проблемы там нет.
Snapshot replay — почему iPad видит то, что я нарисовал минуту назад
Broadcaster держит в памяти массив strokes на каждый slide-id. Когда подключается новый клиент, шлёт ему {type: 'snapshot', slide, strokes}. Дальше — только дельты: каждое движение PointerEvent летит как {type: 'stroke', slide, point, pressure}, broadcaster рассылает всем кроме отправителя. Если я поменял слайд на ноуте — iPad подхватит навигацию через тот же канал и запросит новый snapshot.
Хранилище — обычный объект в памяти node-процесса. Никакой БД, никакой персистентности между рестартами. Это lecture tool, не Miro. Закончил лайв — закрыл вкладку — рисунки умерли. То, что нужно сохранить, я скриню.
Зачем это всё
Я знаю, что мне нужно, и умею это объяснить агенту. Дальше — ровно тот же цикл, что я собирал за вечер через Claude Code для GitHub-хуков: попросил, посмотрел, ткнул пальцем что не так, попросил поправить.
Если процесс повторяется больше двух раз — делегировать. Я провёл первый Урок без рисования и понял: alt-tab в Excalidraw повторится все восемь уроков курса. Значит, нужен инструмент. Personal tool, software for one — не для маркетплейса, не для open-source-релиза. Для одного лайва сегодня в 19:00.
Работает — отлично. Не работает — напишу промпт агенту пофиксить.
Подписаться на обновления — @sereja_tech