From 1db83d12ff717bc2e282fe21d35d282f261147f5 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 4 Oct 2026 10:06:40 +0500 Subject: [PATCH] fix: shapes offset calculate --- docs/backlog.md | 4 ---- web/src/lib/core/shapes.test.ts | 28 ++++++++++++++++++++++++++++ web/src/lib/core/shapes.ts | 18 +++++++++++++++--- web/src/lib/registry/tools/alpha.ts | 4 ++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/backlog.md b/docs/backlog.md index 4b3329e..c48580a 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -65,10 +65,6 @@ строку для svg - форматирование или подсветку синтаксиса??? для bin\hex - форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder - [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone) -- [ ] square-mask-png — перепроверить баг-репорт: он устарел, в схеме больше нет - параметров `width`/`height`/`x`/`y` (теперь `widthPct`/`heightPct` 10..100 - и один `offset` в диапазоне −50..50, значения 100 недостижимы). Либо - подтвердить, что баг исправлен, либо найти новый repro - [ ] прикрутить кастомный color picker, добавить прозрачность (range?) - [ ] проверить настройки range slider на crop-png - слишком большие значения - [ ] для range slider field - добавить поле для ввода цифр и кнопку сброса diff --git a/web/src/lib/core/shapes.test.ts b/web/src/lib/core/shapes.test.ts index 3fc5bda..196efbd 100644 --- a/web/src/lib/core/shapes.test.ts +++ b/web/src/lib/core/shapes.test.ts @@ -59,4 +59,32 @@ describe("renderShape", () => { expect(shifted.data[3]).toBe(0); expect(shifted.data[4 + 3]).toBe(255); }); + + // Регрессия старого бага square-mask: полный размер без смещения обязан + // сохранять квадратную картинку целиком. + it("квадрат 100% без смещения сохраняет всё квадратное изображение", () => { + const out = renderShape( + makeImage(4, 4, new Array(16).fill([255, 255, 255, 255])), + boxTest(0.5, 0.5), + ); + for (let i = 3; i < out.data.length; i += 4) expect(out.data[i]).toBe(255); + }); + + // Смещение — доля свободного места: ±0.5 прижимает фигуру к краю при любом + // её размере, фигура не уезжает за холст. + it("максимальное смещение прижимает фигуру к краю", () => { + const out = renderShape( + makeImage(4, 4, new Array(16).fill([255, 255, 255, 255])), + boxTest(0.25, 0.25), + 0.5, + 0.5, + { x: 0.25, y: 0.25 }, + ); + const opaqueAt = (x: number, y: number) => + out.data[(y * 4 + x) * 4 + 3] === 255; + expect(opaqueAt(3, 3)).toBe(true); + expect(opaqueAt(2, 2)).toBe(true); + expect(opaqueAt(1, 1)).toBe(false); + expect(opaqueAt(2, 1)).toBe(false); + }); }); diff --git a/web/src/lib/core/shapes.ts b/web/src/lib/core/shapes.ts index cffe10b..bf363dd 100644 --- a/web/src/lib/core/shapes.ts +++ b/web/src/lib/core/shapes.ts @@ -3,6 +3,12 @@ import { createPixelImage } from "./types"; export type ShapeTest = (nx: number, ny: number) => boolean; +/** Полуразмер фигуры в нормированных координатах теста, по осям. */ +export interface ShapeExtent { + x: number; + y: number; +} + /** * Координаты теста: нормированные к половине меньшей стороны изображения, * центр в (0,0), ось Y вниз как в пикселях. @@ -54,18 +60,24 @@ export function wavyTest( /** * Вырезает фигуру из изображения: внутри фигуры сохраняются исходные пиксели - * (с их альфой), снаружи альфа обнуляется. Смещение задаётся в долях меньшей стороны. + * (с их альфой), снаружи альфа обнуляется. Смещение задаётся в долях свободного + * места по каждой оси: ±0.5 прижимает фигуру к краю независимо от её размера, + * поэтому фигура никогда не уезжает за холст. Свободное место считается от + * extent (полуразмера фигуры); без extent смещение считается от точки. */ export function renderShape( img: PixelImage, test: ShapeTest, offsetXFrac = 0, offsetYFrac = 0, + extent: ShapeExtent = { x: 0, y: 0 }, ): PixelImage { const out = createPixelImage(img.width, img.height); const minDim = Math.min(img.width, img.height); - const cx = img.width / 2 + offsetXFrac * minDim; - const cy = img.height / 2 + offsetYFrac * minDim; + const freeX = Math.max(0, img.width / (2 * minDim) - extent.x); + const freeY = Math.max(0, img.height / (2 * minDim) - extent.y); + const cx = img.width / 2 + offsetXFrac * 2 * freeX * minDim; + const cy = img.height / 2 + offsetYFrac * 2 * freeY * minDim; for (let y = 0; y < img.height; y++) { for (let x = 0; x < img.width; x++) { const di = (y * img.width + x) * 4; diff --git a/web/src/lib/registry/tools/alpha.ts b/web/src/lib/registry/tools/alpha.ts index 34554af..120fb93 100644 --- a/web/src/lib/registry/tools/alpha.ts +++ b/web/src/lib/registry/tools/alpha.ts @@ -139,6 +139,7 @@ const circleMask: Tool = { circleTest(p.size / 200), p.offset.x / 100, p.offset.y / 100, + { x: p.size / 200, y: p.size / 200 }, ), ), }; @@ -197,6 +198,7 @@ const squareMask: Tool = { boxTest(p.widthPct / 200, p.heightPct / 200), p.offset.x / 100, p.offset.y / 100, + { x: p.widthPct / 200, y: p.heightPct / 200 }, ), ), }; @@ -271,6 +273,7 @@ const starMask: Tool = { starTest(p.points, p.innerRadius / 100, p.size / 200, p.rotation), p.offset.x / 100, p.offset.y / 100, + { x: p.size / 200, y: p.size / 200 }, ), ), }; @@ -345,6 +348,7 @@ const wavyMask: Tool = { wavyTest(p.size / 200, p.amplitude / 200, p.waves, p.phase), p.offset.x / 100, p.offset.y / 100, + { x: (p.size + p.amplitude) / 200, y: (p.size + p.amplitude) / 200 }, ), ), };