Skip to content

Commit 87ff1f0

Browse files
authored
Merge pull request #7 from sourcetoad/position-calculation-man-made
Calculates badge position for circle and square android icons
2 parents bfa431d + 42bb3f4 commit 87ff1f0

20 files changed

+188
-110
lines changed

samples/output/ic_launcher-mdpi.png

36 Bytes
Loading
Loading
56 Bytes
Loading
-137 Bytes
Loading
-136 Bytes
Loading
-97 Bytes
Loading
-161 Bytes
Loading
-46 Bytes
Loading
76 Bytes
Binary file not shown.
1 Byte
Loading
-147 Bytes
Loading
Loading
Loading

src/types/BadgePosition.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Point } from '@imagemagick/magick-wasm';
2+
3+
export default interface BadgePosition {
4+
point: Point;
5+
rotation: number;
6+
}

src/types/Rectangle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default interface Rectangle {
2+
height: number;
3+
width: number;
4+
}

src/utils/addBadgeOverlay.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export default function addBadgeOverlay(
4747
badgeWithShadow,
4848
badgeGravity,
4949
insetWidth,
50-
badge.width,
5150
);
5251

5352
composite.quality = 80;

src/utils/createImageBadgeComposite.ts

Lines changed: 10 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,17 @@ import {
44
IMagickImage,
55
MagickColors,
66
MagickImage,
7-
Point,
87
} from '@imagemagick/magick-wasm';
98

10-
import BadgeGravity, {
11-
getGravityFromBadgeGravity,
12-
} from '../types/BadgeGravity';
13-
import getInsetAtGravity from './getInsetAtGravity';
14-
15-
function getCornerOffset(
16-
insetWidth: number,
17-
finalBadgeWidth: number,
18-
finalBadgeHeight: number,
19-
): number {
20-
// TODO Figure out a better way to calculate this that doesn't rely on magic
21-
// numbers.
22-
return Math.round(insetWidth / 2 - finalBadgeWidth + finalBadgeHeight * 0.2);
23-
}
24-
25-
function isEqualWithinThreshold(
26-
insetX: number,
27-
insetY: number,
28-
threshold = 15,
29-
): boolean {
30-
return Math.abs(((insetY - insetX) * 100) / insetX) <= threshold;
31-
}
9+
import BadgeGravity from '../types/BadgeGravity';
10+
import getCircularBadgePosition from './getCircularBadgePosition';
11+
import getUnequalBadgePosition from './getUnequalBadgePosition';
3212

3313
export default function createImageBadgeComposite(
3414
image: IMagickImage,
3515
badge: IMagickImage,
3616
gravity: BadgeGravity,
3717
insetWidth: number,
38-
badgeWidth: number,
3918
): IMagickImage {
4019
const composite = MagickImage.create();
4120
composite.read(MagickColors.Transparent, image.width, image.height);
@@ -44,97 +23,19 @@ export default function createImageBadgeComposite(
4423
// We need to set a background before rotating, or it may fill it with white.
4524
badge.backgroundColor = MagickColors.None;
4625

47-
switch (gravity) {
48-
case BadgeGravity.Northwest:
49-
case BadgeGravity.Southeast:
50-
badge.rotate(-45);
51-
break;
52-
case BadgeGravity.Northeast:
53-
case BadgeGravity.Southwest:
54-
badge.rotate(45);
55-
break;
56-
}
57-
58-
const edgeScale = 1.1;
59-
const offset = new Point(0, 0);
60-
61-
switch (gravity) {
62-
case BadgeGravity.Northwest:
63-
case BadgeGravity.Northeast: {
64-
const xInset = getInsetAtGravity(
65-
composite,
66-
gravity === BadgeGravity.Northwest ? Gravity.West : Gravity.East,
67-
);
68-
const yInset = getInsetAtGravity(composite, Gravity.North);
69-
70-
if (isEqualWithinThreshold(xInset, yInset)) {
71-
const cornerOffset = getInsetAtGravity(
72-
composite,
73-
Gravity.North,
74-
getCornerOffset(insetWidth, badge.width, badge.height),
75-
);
76-
offset.x = cornerOffset;
77-
offset.y = cornerOffset;
78-
} else {
79-
offset.x = xInset;
80-
offset.y = yInset;
81-
}
82-
break;
83-
}
84-
85-
case BadgeGravity.North:
86-
offset.x = 0;
87-
offset.y = Math.round(
88-
edgeScale *
89-
getInsetAtGravity(
90-
composite,
91-
Gravity.North,
92-
Math.round(badgeWidth / 2),
93-
),
94-
);
95-
break;
96-
97-
case BadgeGravity.Southwest:
98-
case BadgeGravity.Southeast: {
99-
const xInset = getInsetAtGravity(
100-
composite,
101-
gravity === BadgeGravity.Southwest ? Gravity.West : Gravity.East,
102-
);
103-
const yInset = getInsetAtGravity(composite, Gravity.South);
104-
105-
if (isEqualWithinThreshold(xInset, yInset)) {
106-
const cornerOffset = getInsetAtGravity(
107-
composite,
108-
Gravity.South,
109-
getCornerOffset(insetWidth, badge.width, badge.height),
110-
);
111-
offset.x = cornerOffset;
112-
offset.y = cornerOffset;
113-
} else {
114-
offset.x = xInset;
115-
offset.y = yInset;
116-
}
117-
break;
118-
}
26+
const { rotation, point } =
27+
getUnequalBadgePosition(composite, badge, gravity) ??
28+
getCircularBadgePosition(composite, badge, insetWidth / 2, gravity);
11929

120-
case BadgeGravity.South:
121-
offset.x = 0;
122-
offset.y = Math.round(
123-
edgeScale *
124-
getInsetAtGravity(
125-
composite,
126-
Gravity.South,
127-
Math.round(badgeWidth / 2),
128-
),
129-
);
130-
break;
30+
if (rotation) {
31+
badge.rotate(rotation);
13132
}
13233

13334
composite.compositeGravity(
13435
badge,
135-
getGravityFromBadgeGravity(gravity),
36+
Gravity.Northwest,
13637
CompositeOperator.Over,
137-
offset,
38+
point,
13839
);
13940

14041
return composite;

src/utils/getCircularBadgePosition.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Point } from '@imagemagick/magick-wasm';
2+
3+
import BadgeGravity from '../types/BadgeGravity';
4+
import BadgePosition from '../types/BadgePosition';
5+
import Rectangle from '../types/Rectangle';
6+
import getRotatedBadgeInfo from './getRotatedBadgeInfo';
7+
8+
/**
9+
* Returns the position and rotation of the badge assuming the badge is circular
10+
* with equal insets.
11+
*/
12+
export default function getCircularBadgePosition(
13+
container: Rectangle,
14+
badge: Rectangle,
15+
circleRadius: number,
16+
gravity: BadgeGravity,
17+
): BadgePosition {
18+
const { angle, rotation, rotatedWidth, rotatedHeight } = getRotatedBadgeInfo(
19+
badge,
20+
gravity,
21+
);
22+
23+
// Calculate the distance from the center of the container to the bottom-center of
24+
// the badge.
25+
const distance = Math.sqrt(
26+
Math.pow(circleRadius, 2) - Math.pow(badge.width / 2, 2),
27+
);
28+
29+
const trigAngle = ((angle - 90) * Math.PI) / 180;
30+
31+
const x =
32+
container.width / 2 -
33+
(distance - badge.height / 2) * Math.cos(trigAngle) +
34+
-rotatedWidth / 2;
35+
36+
const y =
37+
container.height / 2 +
38+
(-distance + badge.height / 2) * Math.sin(trigAngle) +
39+
-rotatedHeight / 2;
40+
41+
return {
42+
point: new Point(x, y),
43+
rotation,
44+
};
45+
}

src/utils/getRotatedBadgeInfo.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import BadgeGravity from '../types/BadgeGravity';
2+
import Rectangle from '../types/Rectangle';
3+
4+
const badgeGravityAngles: Record<BadgeGravity, number> = {
5+
[BadgeGravity.North]: 180,
6+
[BadgeGravity.Northeast]: -135,
7+
[BadgeGravity.Northwest]: 135,
8+
[BadgeGravity.South]: 0,
9+
[BadgeGravity.Southeast]: -45,
10+
[BadgeGravity.Southwest]: 45,
11+
};
12+
13+
const badgeRotationDegrees: Record<BadgeGravity, number> = {
14+
[BadgeGravity.North]: 0,
15+
[BadgeGravity.Northeast]: 45,
16+
[BadgeGravity.Northwest]: -45,
17+
[BadgeGravity.South]: 0,
18+
[BadgeGravity.Southeast]: -45,
19+
[BadgeGravity.Southwest]: 45,
20+
};
21+
22+
export default function getRotatedBadgeInfo(
23+
badge: Rectangle,
24+
gravity: BadgeGravity,
25+
) {
26+
const angle = badgeGravityAngles[gravity];
27+
const radianAngle = (angle * Math.PI) / 180;
28+
29+
const rotatedWidth =
30+
badge.width * Math.abs(Math.cos(radianAngle)) +
31+
badge.height * Math.abs(Math.sin(radianAngle));
32+
const rotatedHeight =
33+
badge.width * Math.abs(Math.sin(radianAngle)) +
34+
badge.height * Math.abs(Math.cos(radianAngle));
35+
36+
return {
37+
angle,
38+
rotation: badgeRotationDegrees[gravity],
39+
rotatedHeight,
40+
rotatedWidth,
41+
};
42+
}

src/utils/getUnequalBadgePosition.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Gravity, IMagickImage, Point } from '@imagemagick/magick-wasm';
2+
3+
import BadgeGravity from '../types/BadgeGravity';
4+
import BadgePosition from '../types/BadgePosition';
5+
import getInsetAtGravity from './getInsetAtGravity';
6+
import getRotatedBadgeInfo from './getRotatedBadgeInfo';
7+
8+
function isEqualWithinThreshold(
9+
insetX: number,
10+
insetY: number,
11+
threshold = 15,
12+
): boolean {
13+
const percentageDifference = Math.abs(((insetY - insetX) * 100) / insetX);
14+
15+
return percentageDifference <= threshold;
16+
}
17+
18+
/**
19+
* Returns the position and rotation of the badge if the image is found to have
20+
* non-equal insets.
21+
*
22+
* Only supports northeast, northwest, southeast, and southwest.
23+
*/
24+
export default function getUnequalBadgePosition(
25+
container: IMagickImage,
26+
badge: IMagickImage,
27+
gravity: BadgeGravity,
28+
): BadgePosition | undefined {
29+
if (gravity === BadgeGravity.North || gravity === BadgeGravity.South) {
30+
return undefined;
31+
}
32+
33+
const xInset = getInsetAtGravity(
34+
container,
35+
[BadgeGravity.Northwest, BadgeGravity.Southwest].includes(gravity)
36+
? Gravity.West
37+
: Gravity.East,
38+
);
39+
const yInset = getInsetAtGravity(
40+
container,
41+
[BadgeGravity.Northwest, BadgeGravity.Northeast].includes(gravity)
42+
? Gravity.North
43+
: Gravity.South,
44+
);
45+
if (isEqualWithinThreshold(xInset, yInset)) {
46+
return undefined;
47+
}
48+
49+
const { rotation, rotatedWidth, rotatedHeight } = getRotatedBadgeInfo(
50+
badge,
51+
gravity,
52+
);
53+
54+
switch (gravity) {
55+
case BadgeGravity.Northwest:
56+
return {
57+
point: new Point(xInset, yInset),
58+
rotation,
59+
};
60+
case BadgeGravity.Northeast:
61+
return {
62+
point: new Point(container.width - xInset - rotatedWidth, yInset),
63+
rotation,
64+
};
65+
case BadgeGravity.Southwest:
66+
return {
67+
point: new Point(xInset, container.height - yInset - rotatedHeight),
68+
rotation,
69+
};
70+
case BadgeGravity.Southeast:
71+
return {
72+
point: new Point(
73+
container.width - xInset - rotatedWidth,
74+
container.height - yInset - rotatedHeight,
75+
),
76+
rotation,
77+
};
78+
}
79+
80+
return undefined;
81+
}

0 commit comments

Comments
 (0)