Files
easy-png-tools2/docs/plan-fields-audit-fixes.md
T

18 KiB
Raw Blame History

План: исправления по аудиту UX/UI

Статус: draft — на ревью.

Источник: docs/tools-audit.md. Этот документ разбивает аудит на конкретные шаги, группирует по типу работы и фиксирует решения.

0. Ключевое решение: select → buttons

Существующий Segmented.svelte — компонент с группированными кнопками (общая рамка, разделители между сегментами). Для параметров инструментов он не подходит: при 3+ опциях сегменты сжимаются, текст не читается.

Решение: новый компонент ButtonsControl — раздельные outline-кнопки с отступами. Каждая опция — отдельная <button> со своей рамкой, активная подсвечивается заливкой (--color-main). Не зависит от Segmented.

Компонент ButtonsControl

<!-- web/src/lib/components/fields/schema/ButtonsControl.svelte -->
<div class="buttons" role="group">
  {#each options as opt}
    <button
      class="btn"
      class:selected={value === opt.value}
      aria-pressed={value === opt.value}
      onclick={() => select(opt.value)}
    >{opt.label}</button>
  {/each}
</div>
.buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-m);
}
.btn {
  padding: var(--space-m) var(--space-l);
  border: var(--size-border) solid var(--color-border);
  border-radius: var(--radius-s);
  font: var(--font-size-s) var(--font-mono);
  color: var(--color-text-muted);
  background: transparent;
  cursor: pointer;
}
.btn.selected {
  background: var(--color-main);
  color: var(--color-background);
  border-color: var(--color-main);
}

Схема: field.buttons()

Новый тип в registry-schema.ts:

export interface ButtonsSpec<V extends string = string> extends FieldSpecBase {
  kind: "buttons";
  default: V;
  options: { value: V; label: string }[];
}

Фабрика:

buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
  spec: { kind: "buttons", ...s },
}),

Регистрация: добавить "buttons": {} as ButtonsSpec в fieldSpecs, добавить ButtonsControl в FIELDS в SchemaFields.svelte, добавить дефолт/санитайз в sanitizeSchemaParams и defaultSchemaParams.


1. Волна A — инфраструктура (1 коммит)

Шаг A1. Новый тип buttons в схеме

Файлы:

  • web/src/lib/registry-schema.tsButtonsSpec, фабрика field.buttons(), кейс в sanitizeSchemaParams, кейс в defaultSchemaParams
  • web/src/lib/components/fields/schema/ButtonsControl.svelte — новый компонент
  • web/src/lib/components/SchemaFields.svelte — импорт + запись в FIELDS

Шаг A2. Seed → input + randomize

Для field.number() с kind: "seed" (или новый подтип field.seed()) — рендерить поле ввода + кнопку «Random» вместо range slider.

Решение: новый вид field.seed() (наследует от number, но рендерится иначе). Или проще: опциональное поле variant?: "seed" в NumberSpec — тогда RangeControl покажет input + кнопку.

Решение: variant: "seed" в NumberSpec — меньше новых типов.

Файлы:

  • web/src/lib/registry-schema.tsNumberSpec.variant?: "seed"
  • web/src/lib/components/fields/schema/RangeControl.svelte — условный рендер
  • web/src/lib/registry/filters.tsrandomizePixels, addNoise: поле seed → { ...field.number(...), spec: { ...spec, variant: "seed" } } (или пересоздать через field.seed())

2. Волна B — select → buttons (по файлам)

Каждый шаг — один коммит, < 500 строк. Меняем field.select()field.buttons() в перечисленных ниже схемах.

B1. geometry.ts (8 инструментов)

Инструмент Поле Опции
rotate-png angle 90° / 180° / 270°
flip-png axis horizontal / vertical
swap-orientation-png target portrait / landscape
symmetric-copy-png axis vertical / horizontal
symmetric-copy-png keepSide left / right / top / bottom
change-aspect-ratio-png ratio 1:1 / 4:3 / 3:4 / 3:2 / 2:3 / 16:9 / 9:16
change-aspect-ratio-png mode crop / pad

Исключение: change-canvas-size-png (anchor, 9 опций) — оставить field.select(), т.к. 9 кнопок в ряд не поместятся. Аналогично position9 (уже отдельный компонент PositionControl).

B2. color.ts (4 инструмента)

Инструмент Поле Опции
dithering-png pattern floyd-steinberg / bayer
extract-channel-png channel red / green / blue
swap-channels-png pair r-g / r-b / g-b
decrease-color-count-png maxColors 2 / 4 / 8 / 16 / 32 / 64 / 128 / 256

Исключение: channel spaces (hsl/hsv/... component) — 3-6 опций, но это динамические инструменты, оставить select.

B3. analyze.ts (5 инструментов)

Инструмент Поле Опции
show-transparent-png mode binary / highlight
show-grayscale-pixels-png mode (тот же)
show-color-pixels-png mode (тот же)
light-pixel-mask-png mode (тот же)
dark-pixel-mask-png mode (тот же)

Все используют общий maskBaseFields — правка в одном месте.

B4. filters.ts (1 инструмент)

Инструмент Поле Опции
add-noise-png mode mono / color

B5. generate.ts (12 инструментов)

Инструмент Поле Опции
color-spectrum-png direction horizontal / vertical
step-colors-png layout grid / strip
complementary-png layout grid / strip
triadic-png layout grid / strip
tetradic-png layout grid / strip
analogous-png layout grid / strip
monochromatic-png layout grid / strip
shades-png layout grid / strip
sort-colors-png layout grid / strip
mix-colors-png (нет select)

3. Волна C — обязательные фиксы (баги + дефолты)

C1. resize-png: дефолт 0×0 → осмысленный

geometry.ts: width: 0, height: 0width: 512, height: 512 (или width: imgWidth, height: imgHeight — но дефолт в схеме не может знать размер изображения). Решение: дефолт 512×512.

C2. symmetric-copy-png: "keep side" не работает

Баг в run(): keepSide не учитывает axis. При axis: "vertical" (удвоение ширины) работают только left/right, top/bottom не имеют смысла. Нужно: либо фильтровать опции keepSide в зависимости от axis (reactive schema), либо нормализовать в run().

Решение: reactive — при смене axis сбрасывать keepSide на допустимое значение. Пока достаточно в run(): если axis === "vertical" и keepSide ∈ {top, bottom} → заменить на left.

C3. Механизм проброса размеров картинки в схему

Проблема. Поля, значения которых зависят от размеров исходника (crop x/y, resize, offsets), объявлены в статической схеме (-100000…100000), а реальная картинка обычно 800–4000px. Диапазон надо считать от фактического размера изображения, а не от потолков.

Что нужно спроектировать и завести:

  1. Где берём размер. SchemaToolView уже знает размер после decodeFile()PixelImage (width/height). Сейчас он никуда не пробрасывается — надо прокинуть в SchemaFields и дальше в контролы, например через context или проп sourceSize.

  2. Как схеме описать зависимость. В спеках добавить ссылку на размер источника вместо жёстких чисел. Кандидат:

    x: field.slider({
      label: "fields.x",
      bound: "sourceWidth",   // min/max = ±sourceWidth
      default: 0,
    }),
    

    BoundSpec = "sourceWidth" | "sourceHeight" | "minSide" | "maxSide", применимо к NumberSpec, SliderSpec, OffsetSpec, DimensionSpec.

  3. Что делает RangeControl. При bound вычисляет min/max из текущего размера источника; при смене картинки диапазон пересчитывается, текущее значение пере-клампится. Дефолт и sanitize остаются на статической основе (или тоже учат bound) — решить при проектировании.

  4. Первые потребители: crop (x/y → ±sourceWidth/sourceHeight), shift (offsetX/offsetY), resize (max → maxSide).

Это отдельный шаг плана: завести механизм, спроектировать на ревью интерфейс спека (bound vs range: (size) => [min, max]), затем применить к crop и остальным полям размерности. Пока механизма нет — поля crop остаются как есть.

C4. verify-is-png: неверное название

Проверяет текст, не PNG-файл. Переименовать:

  • idverify-is-png-data (или оставить для обратной совместимости)
  • title → "Verify PNG data"
  • description → уточнить

Решение: поменять title и description, id не трогать (url-dependent).


4. Волна D — обязательные фиксы (новые параметры)

D1. remove-alpha-channel-png: выбор цвета фона

Сейчас захардкожен #ffffff. Добавить field.color({ default: "#ffffff" }).

Файл: alpha.ts, схема removeAlphaChannelSchema.

D2. extract-alpha-mask-png: галочка «инвертировать»

Добавить field.checkbox({ label: "fields.invertMask", default: false }). В run(): если invert, вызвать invertAlpha() после extractAlphaMask().

Файл: alpha.ts.

D3. change-canvas-size-png: position9 вместо select

anchor сейчас field.select() с 9 опциями → field.position9().

Файл: geometry.ts. Уже есть PositionControl.

D4. Generators: добавить height

Инструменты без size/height:

  • blend-two-png — добавить height: field.slider({ min: 1, max: 1024, default: 512 })
  • step-colors-png — аналогично
  • complementary, triadic, tetradic, analogous, monochromatic, shades, sort-colors — все используют paletteBaseSchema (общий объект), добавить height туда
  • mix-colors-png — добавить height

Файл: generate.ts.

D5. add-border-png: прозрачность цвета

Текущий field.color() не поддерживает alpha. Пока что: оставить как есть (прозрачность не поддерживается нативным color picker). Отметить в аудите как blocked.


5. Волна E — полировка (некритично)

E1. Плейсхолдер "нет параметров"

Инструменты с пустой схемой (EmptyParams) выглядят странно. Добавить EmptyState или подсказку "No parameters — click Run".

Файл: SchemaToolView.svelte — показать подсказку если schema.fields пуст.

E2. quantize-png: пресеты

field.slider с max 64 → добавить пресеты (8, 16, 32, 64) как кнопки под слайдером. Либо field.buttons() с 4 опциями вместо слайдера.

E3. trim-empty-space-png: 0-254 → проценты

Текущий min: 0, max: 254 — нестандартно. Добавить переключатель px/% (аналог шага A1 — variant: "alpha-threshold" в NumberSpec).

E4. add-text-png: plate offset

Plate снизу имеет большой отступ — добавить галочку "compact plate" или уменьшить дефолтный padding.

E5. emoji-to-png: выбор эмодзи

Поле text → добавить группу часто используемых эмодзи как кнопки- пресеты над полем ввода.


6. Верификация

После каждой волны:

  1. pnpm --dir web build — без ошибок
  2. pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json — без ошибок
  3. pnpm --dir web lint — без ошибок (кроме ожидаемых долгов)
  4. Ручная проверка в браузере: параметры рендерятся, инструменты работают

7. Не покрыто / требует решения

Эти пункты из аудита не попали в волны A–E. Требуют либо дополнительного исследования, либо нормативного решения, либо отложены.

7.1 Требуют normative решения

  • Pixel ↔ Percent (circle-mask, square-mask, star-mask, wavy-mask — %→px, round-corners — px→%): нужен компонент переключателя единиц в слайдере. Отложен до волны E, т.к. требует правки RangeControl + всех схем.
  • Smoothing checkbox для масок (circle, square, star, wavy, round-corners, invert-alpha): нужен field.checkbox("smoothing") + правка core (shapes.ts, alpha). Отложен — требует визуального тестирования.
  • Vignette: выбор центра + цвет + feather — 3 новых параметра. Требует normative решения по UI.
  • Remove-background: выбор точки удаления — либо параметр, либо отдельный инструмент.
  • Precision/quality: select в resize (bilinear/bicubic/nearest) — нужен ли выбор алгоритма?
  • Placeholders: "нет параметров" — какой текст/компонент показывать.

7.2 Операционные (тест-кейсы, описания)

  • find-contour-png: расширять на толщину линии (требует правки core)
  • make-thicker-png: расширять картинку на толщину линии
  • make-thinner-png: уменьшать от краев, fix выступа
  • feather-edges-png: расширять картинку на толщину линии
  • clean-edges-png: найти тест-кейс, возможно увеличить максимум
  • despeckle-alpha-png: найти более заметный тест-кейс
  • close-holes-png: проверить поведение на больших полупрозрачных областях
  • auto-contrast-png: найти тестовый кейс
  • sharpen-png: добавить тест-кейс
  • center-by-alpha-png: найти тест-кейс
  • png-is-grayscale: улучшить видимость результата

7.3 Дизайн-вопросы

  • add-padding-png vs add-border-png: разница неочевидна, нужен review
  • change-aspect-ratio-png: не видно что меняется — рамка результата
  • watermark-tile-png: заполняет не полностью
  • sepia-png: нужна ли настройка силы эффекта?
  • posterize-png: настройка для каждого канала отдельно?
  • pixelate-png: разные алгоритмы?
  • swap-channels-png: обдумать flow через более простые инструменты

7.4 Аудит полей (компоненты)

  • Range slider: поле ввода + кнопки +/- + кнопка сброса
  • Color picker: пипетка с картинки, прозрачность
  • Width/height с keep-aspect-ratio: единый компонент
  • Position offset: отдельные слайдеры x/y (уже сделано в OffsetControl)