← Блог

Свой Miro за 30 минут вместо подписки

Сережа Рис · 27 April 2026

claude-codereactwebsocketcanvasвайбкодинг

Как добавить свой 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, контент разбит на секции.

Поверх контента — слой для рисования:

Добавь WebSocket-сервер для live-sync рисунков по локальной сети:

Отключи 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