I'm trying to draw a circle in JavaScript with Jimp using the code below.
const Jimp = require("jimp");
const size = 500;
const black = [0, 0, 0, 255];
const white = [255, 255, 255, 255];
new Jimp(size, size, (err, image) => {
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
const colorToUse = distanceFromCenter(size, x, y) > size / 2 ? black : white;
const color = Jimp.rgbaToInt(...colorToUse);
image.setPixelColor(color, x, y);
}
}
image.write("circle.png");
});
It produces this.
Problem is, when you zoom in, it looks really choppy.
How can I make the circle smoother and less choppy?
You need to create anti-aliasing. This is easily done for black and white by simply controlling the level of gray of each pixel based on the floating distance it has to the center.
E.g, a pixel with a distance of 250 in your setup should be black, but one with a distance of 250.5 should be gray (~ #808080).
So all you have to do, is to take into account these floating points.
Here is an example using the Canvas2D API, but the core logic is directly applicable to your code.
const size = 500;
const rad = size / 2;
const black = 0xFF000000; //[0, 0, 0, 255];
const white = 0xFFFFFFFF; //[255, 255, 255, 255];
const img = new ImageData(size, size);
const data = new Uint32Array(img.data.buffer);
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
const dist = distanceFromCenter(rad, x, y);
let color;
if (dist >= rad + 1) color = black;
else if (dist <= rad) color = white;
else {
const mult = (255 - Math.floor((dist - rad) * 255)).toString(16).padStart(2, 0);
color = '0xff' + mult.repeat(3); // grayscale 0xffnnnnnn
}
// image.setPixelColor(color, x, y);
data[(y * size) + x] = Number(color);
}
}
//image.write("circle.png");
c.getContext('2d').putImageData(img, 0, 0);
function distanceFromCenter(rad, x, y) {
return Math.hypot(rad - x, rad - y);
}
<canvas id="c" width="500" height="500"></canvas>
I'm sorry to says this but the answer is that you can't really do it. The problem is that a pixel is the minimal unit that can be drawn and you have to either draw it or not. So as long as you use some raster image format (as opposed to vector graphics) you can't draw a smooth line at a big zoom.
If you think about it, you might blame the problem onto the zooming app that doesn't know about the logic of the image (circle) and maps each pixel to many whole pixels. To put it otherwise, your image has only 500x500 pixels of information. You can't reliably build 5,000x5,000 pixels of information (which is effectively what 10x zooming is) from that because there is not enough information in the original image. So you (or whoever does the zooming) have to guess how to fill the missing information and this "chopping" is a result of the simplest (and the most widely used) guessing algorithm there is: just map every pixel on NxN
pixels where N
is zoom factor.
There are three possible workarounds:
Draw a much bigger image so you don't need to zoom it in the first place (but it will take much more space everywhere)
Use some vector graphics like SVG (but you'll have to change the library, and it might be not what you want in the end because there are some other problems with that)
Try to use anti-aliasing which is a clever trick used to subvert how humans see: you draw some pixels around the edge as some gray instead of black-and-white. It will look better at small zooms but at big enough zooms you'll still see the actual details and the magic will stop working.