12 KiB
План начальной реализации: минимальный рабочий MVP
СТАТУС: ВЫПОЛНЕН 2026-08-22.
Что сделано по плану:
- Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с навигацией по категориям ✔
- Шаг 1 — ядро:
PixelImage, декодирование файлов, кодирование PNG/JPEG/WebP, скачивание ✔- Шаг 2 — операции ядра: color / geometry / alpha / analyze, чистые функции без DOM ✔
- Шаг 3 — реестр из 11 инструментов + санитизация параметров ✔
- Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm, DownloadButton, InfoPanel, ToolPage) ✔
- Шаг 5 — маршруты:
/tools/[id]с prerender всех страниц, главная-каталог по категориям ✔- Шаги 6–7 — обработка ошибок, состояния, полировка; верификация ✔
Checkpoint пройден: все 11 инструментов вручную проверены в браузере (загрузка → применение → скачивание), включая найденный и исправленный баг resize с сохранением пропорций. Тесты 75/75, svelte-check 0 ошибок, статический экспорт собирается.
Попутные улучшения сверх плана: корневой скрипт
pnpm dev;sanitizeParamsпротив пустых/невалидных значений полей; семантика «вписать в размеры» для resize.
0. Зафиксированные решения
- Объём MVP: только одиночные инструменты «загрузил → настроил → скачал».
Пайплайн-workspace (
/workspace) вне скоупа этого плана. - Набор инструментов первого среза: ~11 базовых EASY (см. §3). Достаточно, чтобы проверить насквозь ядро → реестр → универсальную страницу.
- Стили: обычный CSS без фреймворка — design-токены на CSS custom properties, ноль зависимостей.
- Стек: SvelteKit +
@sveltejs/adapter-static+ TypeScript, пакетный менеджер pnpm (уже зафиксирован в корневомpackage.json). Весь код сайта — вweb/. - Ядро без DOM: все пиксельные операции — чистые функции над
PixelImage; DOM/canvas допускается только в модуле IO и компонентах превью.
1. Шаг 0 — Каркас проекта
- Создать SvelteKit-приложение в
web/:npx sv create web(шаблон minimal, TypeScript, без лишних аддонов). pnpm installвнутриweb/.svelte.config.js: заменить адаптер на@sveltejs/adapter-static.src/routes/+layout.ts:export const prerender = true;— весь сайт prerenderится в статику.src/app.css: дизайн-токены (палитра, типографика, отступы, радиусы через CSS custom properties) + минимальный сброс стилей.src/routes/+layout.svelte: шапка с названием сайта и навигацией по категориям инструментов (категории — константа из реестра), футер.
Проверка: pnpm dev открывает пустой каркас; pnpm build && pnpm preview
отдаёт статический экспорт.
2. Шаг 1 — Ядро: типы и ввод-вывод
Файлы в web/src/lib/core/:
-
types.tsexport type PixelImage = { width: number; height: number; data: Uint8ClampedArray; }; // RGBA, как ImageData -
io.ts— единственное место ядра, где разрешён canvas/DOM:decodeFile(file: File): Promise<PixelImage>—createImageBitmap(file)→ отрисовка на canvas →getImageData;encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>—putImageData→canvas.toBlob;downloadBlob(blob: Blob, filename: string): void.
Проверка: юнит-смоук — декодировать фикстурный PNG, закодировать обратно, размеры совпадают.
3. Шаг 2 — Операции ядра (чистые функции)
Файлы в web/src/lib/core/, каждая функция: (img, params) => new PixelImage,
без мутации входа:
color.ts:grayscale(img)(luma),invert(img),brightnessContrast(img, brightness −100..100, contrast −100..100).geometry.ts:flip(img, axis),rotate90(img, turns),crop(img, x, y, w, h)— индексная математика;resize(img, w, h)— билинейная интерполяция на TS (неdrawImage, чтобы ядро осталось DOM-free и воспроизводимым бит-в-бит).alpha.ts:removeColorToAlpha(img, hex, tolerancePercent)— цветовое расстояние → альфа 0.analyze.ts:imageInfo(img)→{ width, height, hasAlpha, colorCount }.
Проверка: vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate, crop, grayscale — ожидаемые значения руками.
4. Шаг 3 — Реестр инструментов
web/src/lib/registry.ts — источник истины для страниц, форм и навигации:
export type ParamDef =
| {
id: string;
label: string;
type: "number";
min?: number;
max?: number;
step?: number;
default: number;
}
| {
id: string;
label: string;
type: "select";
options: { value: string; label: string }[];
default: string;
}
| { id: string; label: string; type: "checkbox"; default: boolean }
| { id: string; label: string; type: "color"; default: string };
export type ToolEntry = {
id: string; // slug маршрута
title: string;
description: string;
category: CategoryId;
params: ParamDef[];
run: (
img: PixelImage,
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
};
Категории (константа там же): convert, alpha, color, geometry,
analyze.
Состав первого среза — 11 инструментов
| # | id | Категория | Параметры |
|---|---|---|---|
| 1 | resize-png |
geometry | width, height, checkbox «сохранять пропорции» |
| 2 | crop-png |
geometry | x, y, width, height |
| 3 | rotate-png |
geometry | select: 90 / 180 / 270 |
| 4 | flip-png |
geometry | select: horizontal / vertical |
| 5 | grayscale-png |
color | — |
| 6 | invert-colors-png |
color | — |
| 7 | adjust-brightness-contrast-png |
color | brightness −100..100, contrast −100..100 |
| 8 | convert-png-to-jpg |
convert | background color (подложка под альфу), quality 0..100 |
| 9 | convert-png-to-webp |
convert | quality 0..100 |
| 10 | remove-color-from-png |
alpha | hex color, tolerance % |
| 11 | png-info |
analyze | — особый случай: не картинка, а текстовая сводка |
Для png-info в записи реестра добавляется флаг resultType: 'info'; остальные
— 'image'.
Проверка: импорт реестра в тесте — все id уникальны, у каждого params
имеют дефолты, run определён.
5. Шаг 4 — Универсальные UI-компоненты
web/src/lib/components/:
DropZone.svelte— drag&drop + клик; эмититFile; проверка MIME (PNG/JPEG/WebP/GIF/BMP).Preview.svelte— рендерPixelImageна canvas с fit-масштабированием; шахматный фон под прозрачность.ParamForm.svelte— генерирует контролы изParamDef[], двустороннийbind:values.DownloadButton.svelte— encode выбранного формата +downloadBlob, имя<tool-id>.<ext>.InfoPanel.svelte— текстовый вывод дляresultType: 'info'.ToolPage.svelte— оркестратор страницы инструмента; состоянияidle → loaded → processing → done | error; раскладка: слева DropZone, справа превью + форма параметров + кнопки «Применить» / «Скачать».
Правило: новый инструмент = новая запись в реестре (+ функция операции). Компоненты и маршрут не трогаем.
6. Шаг 5 — Маршруты
/(src/routes/+page.svelte) — сетка карточек инструментов, сгруппированных по категориям; данные из реестра./tools/[id]— универсальная страница:+page.ts:entries()возвращает всеidиз реестра (обязательное условие prerender при adapter-static);load()возвращает запись реестра поid, неизвестный id → 404;+page.svelte— тонкая обёртка надToolPage.
Проверка: pnpm build — в build/tools/ лежат все 11 страниц; главная
содержит ссылки на каждую.
7. Шаг 6 — Ошибки и минимальная полировка
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные параметры crop (выход за границы — клампы или понятная ошибка).
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до загрузки.
- Тексты описаний для всех 11 инструментов (title/description уже в реестре).
8. Шаг 7 — Верификация и checkpoint
Команды:
cd web && pnpm dev # разработка
cd web && pnpm build # статический экспорт должен проходить без ошибок
cd web && pnpm test # vitest: ядро (геометрия/цвет) + целостность реестра
Ручной smoke-тест каждого из 11 инструментов: загрузка PNG → применить → скачать → открыть результат.
Checkpoint MVP: любой из 11 инструментов доступен по прямому URL с главной страницы; PNG загружается drag&drop; параметры применяются; результат скачивается; всё работает офлайн со статического хостинга; ни одной серверной зависимости.