fix: shapes offset calculate

This commit is contained in:
2026-10-04 10:06:40 +05:00
parent ddab996b50
commit 1db83d12ff
4 changed files with 47 additions and 7 deletions
+28
View File
@@ -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);
});
});
+15 -3
View File
@@ -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;
+4
View File
@@ -139,6 +139,7 @@ const circleMask: Tool<CircleMaskParams> = {
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<SquareMaskParams> = {
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<StarMaskParams> = {
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<WavyMaskParams> = {
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 },
),
),
};