Skip to content

Commit 692b155

Browse files
feat: blend pixels using alpha in all draw functions (#478)
Closes: #477
1 parent ec04a57 commit 692b155

12 files changed

+129
-42
lines changed

src/draw/__tests__/drawLineOnImage.test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ test('RGB image', () => {
2020
expect(result).not.toBe(image);
2121
});
2222

23+
test('RGBA image with different alphas', () => {
24+
const image = testUtils.createRgbaImage([
25+
[100, 150, 200, 150, 100, 150, 0, 150],
26+
[100, 200, 5, 150, 3, 200, 0, 150],
27+
[150, 200, 255, 150, 6, 150, 0, 150],
28+
]);
29+
30+
const from = { row: 0, column: 0 };
31+
const to = { row: 1, column: 1 };
32+
const result = image.drawLine(from, to, { strokeColor: [255, 0, 0, 50] });
33+
expect(result).toMatchImageData([
34+
[145, 106, 141, 170, 100, 150, 0, 150],
35+
[100, 200, 5, 150, 76, 141, 0, 170],
36+
[150, 200, 255, 150, 6, 150, 0, 150],
37+
]);
38+
expect(result).not.toBe(image);
39+
});
40+
2341
test('out parameter set to self', () => {
2442
const image = testUtils.createRgbImage([
2543
[100, 150, 200, 100, 150, 0],
@@ -297,6 +315,7 @@ test('different origin, line out of image', () => {
297315
origin: { column: 0, row: 0 },
298316
strokeColor: [1],
299317
});
318+
300319
expect(result).toMatchImageData([
301320
[1, 0, 0, 0],
302321
[0, 1, 0, 0],

src/draw/drawCircleOnImage.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Image } from '../Image';
44
import { Point } from '../utils/geometry/points';
55
import { getDefaultColor } from '../utils/getDefaultColor';
66
import { getOutputImage } from '../utils/getOutputImage';
7+
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
78
import checkProcessable from '../utils/validators/checkProcessable';
89
import { validateColor } from '../utils/validators/validators';
910

@@ -60,20 +61,20 @@ export function drawCircleOnImage(
6061
radius = Math.round(radius);
6162

6263
if (radius === 0) {
63-
newImage.setVisiblePixel(center.column, center.row, color);
64+
setBlendedVisiblePixel(newImage, center.column, center.row, color);
6465
return newImage;
6566
}
6667

6768
if (!fill) {
6869
circle(center.column, center.row, radius, (column: number, row: number) => {
69-
newImage.setVisiblePixel(column, row, color);
70+
setBlendedVisiblePixel(newImage, column, row, color);
7071
});
7172
} else {
7273
if (radius === 1) {
73-
newImage.setVisiblePixel(center.column, center.row, fill);
74+
setBlendedVisiblePixel(newImage, center.column, center.row, fill);
7475
}
7576
circle(center.column, center.row, radius, (column: number, row: number) => {
76-
newImage.setVisiblePixel(column, row, color);
77+
setBlendedVisiblePixel(newImage, column, row, color);
7778

7879
//todo: fill is not optimal we can fill symmetrically
7980
if (column - 1 > center.column) {

src/draw/drawLineOnImage.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Image } from '../Image';
44
import { Point } from '../utils/geometry/points';
55
import { getDefaultColor } from '../utils/getDefaultColor';
66
import { getOutputImage } from '../utils/getOutputImage';
7+
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
78
import checkProcessable from '../utils/validators/checkProcessable';
89
import { validateColor } from '../utils/validators/validators';
910

@@ -56,7 +57,7 @@ export function drawLineOnImage(
5657
Math.round(origin.column + to.column),
5758
Math.round(origin.row + to.row),
5859
(column: number, row: number) => {
59-
newImage.setVisiblePixel(column, row, color);
60+
setBlendedVisiblePixel(newImage, column, row, color);
6061
},
6162
);
6263
return newImage;

src/draw/drawPoints.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Mask } from '../Mask';
33
import { Point } from '../utils/geometry/points';
44
import { getDefaultColor } from '../utils/getDefaultColor';
55
import { getOutputImage, maskToOutputMask } from '../utils/getOutputImage';
6+
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
67
import checkProcessable from '../utils/validators/checkProcessable';
78
import { validateColor } from '../utils/validators/validators';
89

@@ -61,7 +62,8 @@ export function drawPoints(
6162
});
6263

6364
for (const point of points) {
64-
newImage.setVisiblePixel(
65+
setBlendedVisiblePixel(
66+
newImage,
6567
Math.round(origin.column + point.column),
6668
Math.round(origin.row + point.row),
6769
color,

src/draw/drawPolygonOnImage.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Image } from '../Image';
44
import { arrayPointsToObjects } from '../utils/arrayPointsToObjects';
55
import { Point } from '../utils/geometry/points';
66
import { getOutputImage } from '../utils/getOutputImage';
7+
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
78
import checkProcessable from '../utils/validators/checkProcessable';
89
import { validateColor } from '../utils/validators/validators';
910

@@ -61,7 +62,8 @@ export function drawPolygonOnImage(
6162
for (let row = 0; row < newImage.height; row++) {
6263
for (let column = 0; column < newImage.width; column++) {
6364
if (robustPointInPolygon(arrayPoints, [column, row]) === -1) {
64-
newImage.setPixel(
65+
setBlendedVisiblePixel(
66+
newImage,
6567
Math.round(origin.column) + column,
6668
Math.round(origin.row) + row,
6769
fillColor,

src/draw/drawRectangle.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Mask } from '../Mask';
33
import { Point } from '../utils/geometry/points';
44
import { getDefaultColor } from '../utils/getDefaultColor';
55
import { getOutputImage, maskToOutputMask } from '../utils/getOutputImage';
6+
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
67
import checkProcessable from '../utils/validators/checkProcessable';
78

89
export interface DrawRectangleOptions<OutType> {
@@ -82,16 +83,26 @@ export function drawRectangle(
8283
currentColumn < column + width;
8384
currentColumn++
8485
) {
85-
newImage.setVisiblePixel(currentColumn, row, strokeColor);
86-
newImage.setVisiblePixel(currentColumn, row + height - 1, strokeColor);
86+
setBlendedVisiblePixel(newImage, currentColumn, row, strokeColor);
87+
setBlendedVisiblePixel(
88+
newImage,
89+
currentColumn,
90+
row + height - 1,
91+
strokeColor,
92+
);
8793
}
8894
for (
8995
let currentRow = row + 1;
9096
currentRow < row + height - 1;
9197
currentRow++
9298
) {
93-
newImage.setVisiblePixel(column, currentRow, strokeColor);
94-
newImage.setVisiblePixel(column + width - 1, currentRow, strokeColor);
99+
setBlendedVisiblePixel(newImage, column, currentRow, strokeColor);
100+
setBlendedVisiblePixel(
101+
newImage,
102+
column + width - 1,
103+
currentRow,
104+
strokeColor,
105+
);
95106
}
96107
}
97108
if (fillColor !== 'none') {
@@ -105,8 +116,7 @@ export function drawRectangle(
105116
currentColumn < column + width - 1;
106117
currentColumn++
107118
) {
108-
newImage.setVisiblePixel(currentColumn, currentRow, fillColor);
109-
newImage.setVisiblePixel(currentColumn, currentRow, fillColor);
119+
setBlendedVisiblePixel(newImage, currentColumn, currentRow, fillColor);
110120
}
111121
}
112122
}

src/operations/copyTo.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ export function copyTo(
6969
currentColumn - column,
7070
currentRow - row,
7171
);
72-
setBlendedPixel(result, currentColumn, currentRow, {
73-
color: sourcePixel,
74-
});
72+
setBlendedPixel(result, currentColumn, currentRow, sourcePixel);
7573
}
7674
}
7775

src/operations/paintMaskOnImage.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ export function paintMaskOnImage(
6868
currentColumn++
6969
) {
7070
if (mask.getBit(currentColumn - column, currentRow - row)) {
71-
setBlendedPixel(result, currentColumn, currentRow, {
72-
color,
73-
});
71+
setBlendedPixel(result, currentColumn, currentRow, color);
7472
}
7573
}
7674
}

src/utils/__tests__/setBlendedPixel.test.ts

+4-12
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,23 @@ test('GREYA image, default options', () => {
1414

1515
test('GREYA images: transparent source, opaque target', () => {
1616
const image = testUtils.createGreyaImage([[50, 255]]);
17-
setBlendedPixel(image, 0, 0, { color: [100, 0] });
17+
setBlendedPixel(image, 0, 0, [100, 0]);
1818
expect(image).toMatchImageData([[50, 255]]);
1919
});
2020

2121
test('GREYA images: opaque source, transparent target', () => {
2222
const image = testUtils.createGreyaImage([[50, 0]]);
23-
setBlendedPixel(image, 0, 0, { color: [100, 255] });
23+
setBlendedPixel(image, 0, 0, [100, 255]);
2424
expect(image).toMatchImageData([[100, 255]]);
2525
});
2626

27-
test('GREYA image: alpha different from 255', () => {
28-
const image = testUtils.createGreyaImage([[50, 64]]);
29-
setBlendedPixel(image, 0, 0, { color: [100, 128] });
30-
const alpha = 128 + 64 * (1 - 128 / 255);
31-
const component = (100 * 128 + 50 * 64 * (1 - 128 / 255)) / alpha;
32-
expect(image).toMatchImageData([[component, alpha]]);
33-
});
34-
3527
test('asymetrical test', () => {
3628
const image = testUtils.createGreyaImage([
3729
[50, 255, 1, 2, 3, 4],
3830
[20, 30, 5, 6, 7, 8],
3931
[1, 2, 3, 4, 5, 6],
4032
]);
41-
setBlendedPixel(image, 2, 0, { color: [0, 125] });
33+
setBlendedPixel(image, 2, 0, [0, 125]);
4234
expect(image).toMatchImageData([
4335
[50, 255, 1, 2, 0, 127],
4436
[20, 30, 5, 6, 7, 8],
@@ -60,7 +52,7 @@ test('2x2 mask, color is 1', () => {
6052
[1, 0],
6153
[0, 0],
6254
]);
63-
setBlendedPixel(mask, 1, 0, { color: [1] });
55+
setBlendedPixel(mask, 1, 0, [1]);
6456

6557
expect(mask).toMatchMaskData([
6658
[1, 1],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { setBlendedVisiblePixel } from '../setBlendedVisiblePixel';
2+
3+
test('GREYA image, default options', () => {
4+
const image = testUtils.createGreyaImage([
5+
[50, 255],
6+
[20, 30],
7+
]);
8+
setBlendedVisiblePixel(image, 0, 1);
9+
expect(image).toMatchImageData([
10+
[50, 255],
11+
[0, 255],
12+
]);
13+
});
14+
15+
test('GREYA image: set pixel out of bounds', () => {
16+
const data = [
17+
[50, 255, 1, 2, 3, 4],
18+
[20, 30, 5, 6, 7, 8],
19+
[1, 2, 3, 4, 5, 6],
20+
];
21+
const image = testUtils.createGreyaImage(data);
22+
setBlendedVisiblePixel(image, 0, 5, [40, 40]);
23+
expect(image).toMatchImageData(data);
24+
});
25+
26+
test('RGBA image: set pixel out of bounds', () => {
27+
const data = [
28+
[50, 255, 1, 200, 2, 3, 4, 200],
29+
[20, 30, 5, 200, 6, 7, 8, 200],
30+
[1, 2, 3, 200, 4, 5, 6, 200],
31+
];
32+
const image = testUtils.createGreyaImage(data);
33+
setBlendedVisiblePixel(image, 0, 5, [40, 40, 40, 40]);
34+
expect(image).toMatchImageData(data);
35+
});
36+
37+
test('asymetrical test', () => {
38+
const image = testUtils.createGreyaImage([
39+
[50, 255, 1, 2, 3, 4],
40+
[20, 30, 5, 6, 7, 8],
41+
[1, 2, 3, 4, 5, 6],
42+
]);
43+
setBlendedVisiblePixel(image, 2, 0, [0, 125]);
44+
expect(image).toMatchImageData([
45+
[50, 255, 1, 2, 0, 127],
46+
[20, 30, 5, 6, 7, 8],
47+
[1, 2, 3, 4, 5, 6],
48+
]);
49+
});

src/utils/setBlendedPixel.ts

+4-11
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,21 @@ import { Mask } from '../Mask';
44
import { getDefaultColor } from './getDefaultColor';
55
import { assert } from './validators/assert';
66

7-
export interface SetBlendedPixelOptions {
8-
/**
9-
* Color with which to blend the image pixel.
10-
* @default `'Opaque black'`.
11-
*/
12-
color?: number[];
13-
}
14-
157
/**
168
* Blend the given pixel with the pixel at the specified location in the image.
179
* @param image - The image with which to blend.
1810
* @param column - Column of the target pixel.
1911
* @param row - Row of the target pixel.
20-
* @param options - Set blended pixel options.
12+
* @param color - Color with which to blend the image pixel. @default `'Opaque black'`.
2113
*/
14+
2215
export function setBlendedPixel(
2316
image: Image | Mask,
2417
column: number,
2518
row: number,
26-
options: SetBlendedPixelOptions = {},
19+
color?: number[],
2720
) {
28-
const { color = getDefaultColor(image) } = options;
21+
color = color ?? getDefaultColor(image);
2922

3023
if (!image.alpha) {
3124
image.setPixel(column, row, color);

src/utils/setBlendedVisiblePixel.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Image } from '../Image';
2+
import { Mask } from '../Mask';
3+
4+
import { setBlendedPixel } from './setBlendedPixel';
5+
6+
/**
7+
* Blend the given pixel with the pixel at the specified location in the image if the pixel is in image's bounds.
8+
* @param image - The image with which to blend.
9+
* @param column - Column of the target pixel.
10+
* @param row - Row of the target pixel.
11+
* @param color - Color with which to blend the image pixel. @default `'Opaque black'`.
12+
*/
13+
export function setBlendedVisiblePixel(
14+
image: Image | Mask,
15+
column: number,
16+
row: number,
17+
color?: number[],
18+
) {
19+
if (column >= 0 && column < image.width && row >= 0 && row < image.height) {
20+
setBlendedPixel(image, column, row, color);
21+
}
22+
}

0 commit comments

Comments
 (0)