Веб-редактор для рисования, создания покадровой анимации и экспорта коротких мультиков прямо в браузере.
О проекте · Возможности · Быстрый старт · Архитектура · Деплой
Мульти-Пульти - это браузерная творческая студия для детского рисования и простой покадровой анимации. Пользователь рисует на HTML5 Canvas, добавляет кадры на таймлайн, проигрывает результат как мультик и сохраняет работу в PNG или GIF.
Приложение не требует серверной части для основной работы. Все пользовательское состояние хранится локально в браузере через localStorage, а графика обрабатывается на клиенте. Это упрощает запуск, деплой и демонстрацию проекта: достаточно собрать статический сайт и отдать папку dist.
Интерфейс построен вокруг одного рабочего экрана:
- сверху - основное меню с логотипом, отменой/повтором, загрузкой изображения, очисткой и экспортом;
- слева - основные инструменты рисования;
- рядом - контекстная панель настроек выбранного инструмента;
- в центре - холст и временный overlay-слой для предпросмотра действий;
- снизу - таймлайн кадров и управление воспроизведением.
- Кисть с несколькими размерами.
- Ластик, который работает как рисование белым цветом.
- Заливка области выбранным цветом.
- Пипетка для выбора цвета с холста.
- Симметричное рисование по горизонтали.
- Умный контур: свободный штрих может быть преобразован в ровную линию или эллипс.
- Базовая палитра цветов.
- Недавние цвета, которые обновляются при выборе.
- Избранные цвета.
- Кастомный выбор цвета через hue-слайдер и поле насыщенности/яркости.
- Перекраска выбранного объекта через режим выделения.
- Линия, круг и прямоугольник.
- Добавление текста с выбором шрифта.
- Редактирование ранее добавленного текста, если он распознан как выделенный объект.
- Стикеры из встроенных категорий.
- Перемещение, масштабирование, отражение и удаление выделенных элементов.
- Создание нескольких кадров.
- Копирование текущего кадра.
- Удаление кадра, если в проекте больше одного кадра.
- Перетаскивание кадров на таймлайне.
- Воспроизведение анимации с разной скоростью.
- Экспорт всей последовательности кадров в GIF.
- Экспорт текущего кадра в PNG.
- Экспорт анимации в GIF.
- Загрузка изображения на текущий кадр.
- Автосохранение истории, текущего кадра, недавних и избранных цветов в
localStorage.
В проекте есть базовый фильтр содержимого кадра. Он не использует сервер и не отправляет рисунки наружу. Фильтр анализирует пиксели текущего canvas и оценивает несколько грубых признаков:
- агрессивную палитру, где почти весь рисунок состоит из черного и красного;
- длинные пересекающиеся линии и X-образные фигуры;
- плотное хаотичное зачеркивание по всему холсту;
- сильную четырехкратную симметрию как дополнительный риск-сигнал.
Фильтр эвристический: он нужен как мягкая защита от явно нежелательных рисунков, а не как полноценная система модерации. При блокировке кадр очищается или удаляется, а история переписывается, чтобы действие отмены не вернуло заблокированное содержимое.
- React 19
- TypeScript
- Vite 6
- Tailwind CSS 4
- HTML5 Canvas
- gifenc
- lucide-react
- clsx
- tailwind-merge
- Node.js 20 или новее
- npm
npm installnpm run devПо умолчанию Vite запускается на порту 3000:
http://localhost:3000
Скрипт запуска использует --host=0.0.0.0, поэтому приложение также может быть доступно по локальному сетевому адресу машины.
npm run devЗапускает Vite dev-сервер на 3000 порту.
npm run lintЗапускает проверку TypeScript без генерации файлов: tsc --noEmit.
npm testПроверяет базовые безопасный и блокируемый сценарии пиксельного content-фильтра и контракт PNG-экспорта.
npm run buildСобирает production-версию приложения в папку dist.
npm run previewЗапускает Vite preview для проверки уже собранной версии.
npm run cleanУдаляет папку dist. В текущем виде команда использует rm -rf, поэтому в Windows ее удобнее запускать из Git Bash, WSL или другой оболочки, где доступна команда rm.
src
├── canvas
│ └── operations.ts
├── components
│ ├── DrawingStage.tsx
│ ├── EditorModals.tsx
│ ├── HeaderToolbar.tsx
│ ├── Timeline.tsx
│ ├── ToolSettingsPanel.tsx
│ └── ToolsPanel.tsx
├── constants
│ └── editor.ts
├── hooks
│ ├── useAnimationPlayback.ts
│ ├── useContentModeration.ts
│ ├── useFrameHistory.ts
│ ├── useKeyboardShortcuts.ts
│ └── useProjectPersistence.ts
├── services
│ ├── imageExport.ts
│ └── projectStorage.ts
├── types
│ └── editor.ts
├── utils
│ ├── audio.ts
│ ├── cn.ts
│ ├── contentFilter.ts
│ ├── extractObject.ts
│ ├── floodFill.ts
│ ├── gifExport.ts
│ └── shapeDetection.ts
├── App.tsx
├── index.css
└── main.tsx
Координатор редактора: связывает состояние активного инструмента и временных объектов с Canvas-обработчиками, хуками истории и воспроизведения, сервисами и визуальными компонентами. Разметка отдельных панелей в нем не хранится.
Canvas разделен на два слоя:
- основной canvas хранит фактический кадр;
- overlay canvas используется для предпросмотра кисти, фигур, текста, стикеров и выделений до финального сохранения.
Компоненты отвечают за отображение и передачу UI-событий: верхнюю панель, выбор инструмента, контекстные настройки, Canvas-сцену, таймлайн и модальные панели. Состояние истории и низкоуровневые операции в них не реализуются.
useFrameHistory управляет кадрами, снимками и ветвлением undo/redo. useAnimationPlayback изолирует таймер воспроизведения, useProjectPersistence синхронизирует проект с browser storage, useContentModeration координирует проверку кадра, а useKeyboardShortcuts подписывает приложение на сочетания клавиш без устаревших замыканий.
Чистые и DOM-ориентированные операции Canvas: координаты, пустой кадр, сглаженный штрих, геометрические фигуры, цветовые преобразования и проверка попадания в выделение. Модуль не зависит от React.
projectStorage.ts валидирует и сохраняет проект в localStorage, включая резервный компактный вариант. imageExport.ts отвечает за скачивание PNG/GIF и освобождение временных object URL.
Статические данные редактора и общие TypeScript-модели вынесены из визуального слоя. Переиспользуемые сущности экспортируются по именам.
Специализированные алгоритмы заливки, извлечения объекта, распознавания простых фигур, пиксельной проверки содержимого, GIF-кодирования, звуков интерфейса и объединения CSS-классов.
Глобальные стили и Tailwind-утилиты. Здесь определена основная кнопка btn-kid, активное состояние кнопок, стили color slider и запрет мобильных browser-жестов во время рисования.
В проекте используются два места для изображений:
assets- изображения, которые импортируются из кода или используются в README;public- статические файлы, которые должны быть доступны из корня сайта.
Логотип лежит в двух местах:
assets/MultiPulit-Logo.png- используется внутри React-приложения в верхнемheader;public/MultiPulit-Logo.png- используется как favicon иapple-touch-iconвindex.html.
Если нужно заменить логотип, замените оба файла или обновите пути в src/constants/editor.ts и index.html.
Основное состояние проекта управляется хуком useFrameHistory и хранится в массиве истории:
{
frames: string[];
}Каждый кадр хранится как PNG dataURL. Это удобно для простого клиентского приложения: кадр легко отрисовать обратно на canvas, сохранить в localStorage и передать в GIF-экспортер.
В localStorage сохраняются:
- история кадров;
- текущая позиция в истории;
- текущий кадр;
- избранные цвета;
- недавние цвета.
Если полная история становится слишком большой для localStorage, приложение пытается сохранить минимальное состояние с текущим кадром.
Ctrl+Z/Cmd+Z— отменить действие.Ctrl+Shift+Z/Cmd+Shift+Z— повторить действие.Ctrl+Y/Cmd+Y— повторить действие в Windows-стиле.
Сочетания не перехватываются, когда фокус находится в поле ввода текста.
- Выберите кисть.
- Выберите толщину и цвет.
- Нарисуйте на холсте.
- При отпускании указателя кадр сохраняется в историю.
- Нарисуйте первый кадр.
- Нажмите кнопку добавления кадра.
- Нарисуйте следующий кадр.
- Повторите несколько раз.
- Нажмите воспроизведение на таймлайне.
- При необходимости выберите скорость.
- PNG сохраняет только текущий кадр.
- GIF сохраняет всю последовательность кадров.
- Для GIF нужен минимум два кадра.
- Выберите инструмент перемещения.
- Нажмите на нарисованный объект.
- Переместите, масштабируйте, отразите или перекрасьте выделение.
- При смене инструмента выделение финализируется на основном canvas.
Проверка типов:
npm run lintProduction-сборка:
npm run buildПосле сборки результат появляется в папке:
dist
Эту папку можно отдать любым статическим сервером.
В проекте есть конфигурация для Render:
services:
- type: web
name: multi-pulti
env: static
buildCommand: npm install && npm run build
staticPublishPath: ./dist
routes:
- type: rewrite
source: /*
destination: /index.htmlДля другого статического хостинга обычно достаточно:
- Запустить
npm install. - Запустить
npm run build. - Опубликовать содержимое папки
dist. - Настроить fallback на
index.html, если хостинг требует явной настройки SPA-роутинга.
Минимальный чеклист:
npm run lintnpm run build- открыть приложение в браузере;
- проверить рисование кистью;
- проверить заливку;
- добавить, скопировать и удалить кадр;
- включить воспроизведение;
- добавить текст;
- добавить стикер;
- загрузить изображение;
- сохранить PNG;
- создать минимум два кадра и сохранить GIF.
Остановите процесс, который уже использует порт, или временно измените порт в скрипте dev в package.json.
Браузеры агрессивно кешируют favicon. После замены public/MultiPulit-Logo.png попробуйте:
- жестко обновить страницу;
- открыть сайт в приватном окне;
- очистить кеш сайта;
- временно добавить версию к пути favicon в
index.html, например/MultiPulit-Logo.png?v=2.
Прогресс хранится в localStorage конкретного браузера и домена. Он может пропасть после очистки данных сайта, запуска на другом домене или в другом браузере.
Проверьте, что в проекте больше одного кадра. Экспорт GIF намеренно не запускается для одного кадра.
В некоторых ограниченных окружениях Vite может не получить право запустить дочерний процесс esbuild. В обычной локальной среде это решается запуском команды без sandbox-ограничений.
Проект распространяется под лицензией MIT. Подробности указаны в файле LICENSE.
