Skip to content

Commit

Permalink
feat: blend pixels using alpha in all draw functions (#478)
Browse files Browse the repository at this point in the history
Closes: #477
  • Loading branch information
EscapedGibbon authored Oct 3, 2024
1 parent ec04a57 commit 692b155
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 42 deletions.
19 changes: 19 additions & 0 deletions src/draw/__tests__/drawLineOnImage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ test('RGB image', () => {
expect(result).not.toBe(image);
});

test('RGBA image with different alphas', () => {
const image = testUtils.createRgbaImage([
[100, 150, 200, 150, 100, 150, 0, 150],
[100, 200, 5, 150, 3, 200, 0, 150],
[150, 200, 255, 150, 6, 150, 0, 150],
]);

const from = { row: 0, column: 0 };
const to = { row: 1, column: 1 };
const result = image.drawLine(from, to, { strokeColor: [255, 0, 0, 50] });
expect(result).toMatchImageData([
[145, 106, 141, 170, 100, 150, 0, 150],
[100, 200, 5, 150, 76, 141, 0, 170],
[150, 200, 255, 150, 6, 150, 0, 150],
]);
expect(result).not.toBe(image);
});

test('out parameter set to self', () => {
const image = testUtils.createRgbImage([
[100, 150, 200, 100, 150, 0],
Expand Down Expand Up @@ -297,6 +315,7 @@ test('different origin, line out of image', () => {
origin: { column: 0, row: 0 },
strokeColor: [1],
});

expect(result).toMatchImageData([
[1, 0, 0, 0],
[0, 1, 0, 0],
Expand Down
9 changes: 5 additions & 4 deletions src/draw/drawCircleOnImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Image } from '../Image';
import { Point } from '../utils/geometry/points';
import { getDefaultColor } from '../utils/getDefaultColor';
import { getOutputImage } from '../utils/getOutputImage';
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
import checkProcessable from '../utils/validators/checkProcessable';
import { validateColor } from '../utils/validators/validators';

Expand Down Expand Up @@ -60,20 +61,20 @@ export function drawCircleOnImage(
radius = Math.round(radius);

if (radius === 0) {
newImage.setVisiblePixel(center.column, center.row, color);
setBlendedVisiblePixel(newImage, center.column, center.row, color);
return newImage;
}

if (!fill) {
circle(center.column, center.row, radius, (column: number, row: number) => {
newImage.setVisiblePixel(column, row, color);
setBlendedVisiblePixel(newImage, column, row, color);
});
} else {
if (radius === 1) {
newImage.setVisiblePixel(center.column, center.row, fill);
setBlendedVisiblePixel(newImage, center.column, center.row, fill);
}
circle(center.column, center.row, radius, (column: number, row: number) => {
newImage.setVisiblePixel(column, row, color);
setBlendedVisiblePixel(newImage, column, row, color);

//todo: fill is not optimal we can fill symmetrically

Check warning on line 79 in src/draw/drawCircleOnImage.ts

View workflow job for this annotation

GitHub Actions / nodejs / lint-eslint

Unexpected 'todo' comment: 'todo: fill is not optimal we can fill...'
if (column - 1 > center.column) {
Expand Down
3 changes: 2 additions & 1 deletion src/draw/drawLineOnImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Image } from '../Image';
import { Point } from '../utils/geometry/points';
import { getDefaultColor } from '../utils/getDefaultColor';
import { getOutputImage } from '../utils/getOutputImage';
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
import checkProcessable from '../utils/validators/checkProcessable';
import { validateColor } from '../utils/validators/validators';

Expand Down Expand Up @@ -56,7 +57,7 @@ export function drawLineOnImage(
Math.round(origin.column + to.column),
Math.round(origin.row + to.row),
(column: number, row: number) => {
newImage.setVisiblePixel(column, row, color);
setBlendedVisiblePixel(newImage, column, row, color);
},
);
return newImage;
Expand Down
4 changes: 3 additions & 1 deletion src/draw/drawPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Mask } from '../Mask';
import { Point } from '../utils/geometry/points';
import { getDefaultColor } from '../utils/getDefaultColor';
import { getOutputImage, maskToOutputMask } from '../utils/getOutputImage';
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
import checkProcessable from '../utils/validators/checkProcessable';
import { validateColor } from '../utils/validators/validators';

Expand Down Expand Up @@ -61,7 +62,8 @@ export function drawPoints(
});

for (const point of points) {
newImage.setVisiblePixel(
setBlendedVisiblePixel(
newImage,
Math.round(origin.column + point.column),
Math.round(origin.row + point.row),
color,
Expand Down
4 changes: 3 additions & 1 deletion src/draw/drawPolygonOnImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Image } from '../Image';
import { arrayPointsToObjects } from '../utils/arrayPointsToObjects';
import { Point } from '../utils/geometry/points';
import { getOutputImage } from '../utils/getOutputImage';
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
import checkProcessable from '../utils/validators/checkProcessable';
import { validateColor } from '../utils/validators/validators';

Expand Down Expand Up @@ -61,7 +62,8 @@ export function drawPolygonOnImage(
for (let row = 0; row < newImage.height; row++) {
for (let column = 0; column < newImage.width; column++) {
if (robustPointInPolygon(arrayPoints, [column, row]) === -1) {
newImage.setPixel(
setBlendedVisiblePixel(
newImage,
Math.round(origin.column) + column,
Math.round(origin.row) + row,
fillColor,
Expand Down
22 changes: 16 additions & 6 deletions src/draw/drawRectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Mask } from '../Mask';
import { Point } from '../utils/geometry/points';
import { getDefaultColor } from '../utils/getDefaultColor';
import { getOutputImage, maskToOutputMask } from '../utils/getOutputImage';
import { setBlendedVisiblePixel } from '../utils/setBlendedVisiblePixel';
import checkProcessable from '../utils/validators/checkProcessable';

export interface DrawRectangleOptions<OutType> {
Expand Down Expand Up @@ -82,16 +83,26 @@ export function drawRectangle(
currentColumn < column + width;
currentColumn++
) {
newImage.setVisiblePixel(currentColumn, row, strokeColor);
newImage.setVisiblePixel(currentColumn, row + height - 1, strokeColor);
setBlendedVisiblePixel(newImage, currentColumn, row, strokeColor);
setBlendedVisiblePixel(
newImage,
currentColumn,
row + height - 1,
strokeColor,
);
}
for (
let currentRow = row + 1;
currentRow < row + height - 1;
currentRow++
) {
newImage.setVisiblePixel(column, currentRow, strokeColor);
newImage.setVisiblePixel(column + width - 1, currentRow, strokeColor);
setBlendedVisiblePixel(newImage, column, currentRow, strokeColor);
setBlendedVisiblePixel(
newImage,
column + width - 1,
currentRow,
strokeColor,
);
}
}
if (fillColor !== 'none') {
Expand All @@ -105,8 +116,7 @@ export function drawRectangle(
currentColumn < column + width - 1;
currentColumn++
) {
newImage.setVisiblePixel(currentColumn, currentRow, fillColor);
newImage.setVisiblePixel(currentColumn, currentRow, fillColor);
setBlendedVisiblePixel(newImage, currentColumn, currentRow, fillColor);
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/operations/copyTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ export function copyTo(
currentColumn - column,
currentRow - row,
);
setBlendedPixel(result, currentColumn, currentRow, {
color: sourcePixel,
});
setBlendedPixel(result, currentColumn, currentRow, sourcePixel);
}
}

Expand Down
4 changes: 1 addition & 3 deletions src/operations/paintMaskOnImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ export function paintMaskOnImage(
currentColumn++
) {
if (mask.getBit(currentColumn - column, currentRow - row)) {
setBlendedPixel(result, currentColumn, currentRow, {
color,
});
setBlendedPixel(result, currentColumn, currentRow, color);
}
}
}
Expand Down
16 changes: 4 additions & 12 deletions src/utils/__tests__/setBlendedPixel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,23 @@ test('GREYA image, default options', () => {

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

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

test('GREYA image: alpha different from 255', () => {
const image = testUtils.createGreyaImage([[50, 64]]);
setBlendedPixel(image, 0, 0, { color: [100, 128] });
const alpha = 128 + 64 * (1 - 128 / 255);
const component = (100 * 128 + 50 * 64 * (1 - 128 / 255)) / alpha;
expect(image).toMatchImageData([[component, alpha]]);
});

test('asymetrical test', () => {
const image = testUtils.createGreyaImage([
[50, 255, 1, 2, 3, 4],
[20, 30, 5, 6, 7, 8],
[1, 2, 3, 4, 5, 6],
]);
setBlendedPixel(image, 2, 0, { color: [0, 125] });
setBlendedPixel(image, 2, 0, [0, 125]);
expect(image).toMatchImageData([
[50, 255, 1, 2, 0, 127],
[20, 30, 5, 6, 7, 8],
Expand All @@ -60,7 +52,7 @@ test('2x2 mask, color is 1', () => {
[1, 0],
[0, 0],
]);
setBlendedPixel(mask, 1, 0, { color: [1] });
setBlendedPixel(mask, 1, 0, [1]);

expect(mask).toMatchMaskData([
[1, 1],
Expand Down
49 changes: 49 additions & 0 deletions src/utils/__tests__/setBlendedVisiblePixel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { setBlendedVisiblePixel } from '../setBlendedVisiblePixel';

test('GREYA image, default options', () => {
const image = testUtils.createGreyaImage([
[50, 255],
[20, 30],
]);
setBlendedVisiblePixel(image, 0, 1);
expect(image).toMatchImageData([
[50, 255],
[0, 255],
]);
});

test('GREYA image: set pixel out of bounds', () => {
const data = [
[50, 255, 1, 2, 3, 4],
[20, 30, 5, 6, 7, 8],
[1, 2, 3, 4, 5, 6],
];
const image = testUtils.createGreyaImage(data);
setBlendedVisiblePixel(image, 0, 5, [40, 40]);
expect(image).toMatchImageData(data);
});

test('RGBA image: set pixel out of bounds', () => {
const data = [
[50, 255, 1, 200, 2, 3, 4, 200],
[20, 30, 5, 200, 6, 7, 8, 200],
[1, 2, 3, 200, 4, 5, 6, 200],
];
const image = testUtils.createGreyaImage(data);
setBlendedVisiblePixel(image, 0, 5, [40, 40, 40, 40]);
expect(image).toMatchImageData(data);
});

test('asymetrical test', () => {
const image = testUtils.createGreyaImage([
[50, 255, 1, 2, 3, 4],
[20, 30, 5, 6, 7, 8],
[1, 2, 3, 4, 5, 6],
]);
setBlendedVisiblePixel(image, 2, 0, [0, 125]);
expect(image).toMatchImageData([
[50, 255, 1, 2, 0, 127],
[20, 30, 5, 6, 7, 8],
[1, 2, 3, 4, 5, 6],
]);
});
15 changes: 4 additions & 11 deletions src/utils/setBlendedPixel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,21 @@ import { Mask } from '../Mask';
import { getDefaultColor } from './getDefaultColor';
import { assert } from './validators/assert';

export interface SetBlendedPixelOptions {
/**
* Color with which to blend the image pixel.
* @default `'Opaque black'`.
*/
color?: number[];
}

/**
* Blend the given pixel with the pixel at the specified location in the image.
* @param image - The image with which to blend.
* @param column - Column of the target pixel.
* @param row - Row of the target pixel.
* @param options - Set blended pixel options.
* @param color - Color with which to blend the image pixel. @default `'Opaque black'`.
*/

export function setBlendedPixel(
image: Image | Mask,
column: number,
row: number,
options: SetBlendedPixelOptions = {},
color?: number[],
) {
const { color = getDefaultColor(image) } = options;
color = color ?? getDefaultColor(image);

if (!image.alpha) {
image.setPixel(column, row, color);
Expand Down
22 changes: 22 additions & 0 deletions src/utils/setBlendedVisiblePixel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Image } from '../Image';
import { Mask } from '../Mask';

import { setBlendedPixel } from './setBlendedPixel';

/**
* Blend the given pixel with the pixel at the specified location in the image if the pixel is in image's bounds.
* @param image - The image with which to blend.
* @param column - Column of the target pixel.
* @param row - Row of the target pixel.
* @param color - Color with which to blend the image pixel. @default `'Opaque black'`.
*/
export function setBlendedVisiblePixel(
image: Image | Mask,
column: number,
row: number,
color?: number[],
) {
if (column >= 0 && column < image.width && row >= 0 && row < image.height) {
setBlendedPixel(image, column, row, color);
}
}

0 comments on commit 692b155

Please sign in to comment.