Draw a line in a bitmap (possibly with piston) - rust

I want to draw a line in a bitmap, e.g. from pixel (10, 10) to pixel (90, 90). The line must have a specific width.
Using piston image, I am able to draw a single pixel:
let mut image = ImageBuffer::<image::Rgb<u8>>::new(100, 100);
image.get_pixel_mut(5, 5).data = [255, 255, 255];
image.save("output.png");
However there is no method to draw a line.
I suppose I have to use piston::graphics for that, but I can’t find any ressource how to do it (any example involves a window that provides a context on which graphics works on).

In addition to the great answer above: there is now direct support for drawing lines and many more shapes (even texts) in the imageproc library (see also the examples there):
extern crate image;
extern crate imageproc;
use image::{Rgb, RgbImage};
use imageproc::drawing::draw_line_segment_mut;
fn main() {
let mut img = RgbImage::new(100, 100);
draw_line_segment_mut(
&mut img,
(5f32, 5f32), // start point
(95f32, 95f32), // end point
Rgb([69u8, 203u8, 133u8]), // RGB colors
);
img.save("output.png").unwrap();
}

If you drop the width requirement and don't need antialiasing either, you could use something like Bresenham's line algorithm (also on Rosetta Code):
extern crate image;
use image::RgbImage;
fn draw_line(img: &mut RgbImage, x0: i64, y0: i64, x1: i64, y1: i64) {
// Create local variables for moving start point
let mut x0 = x0;
let mut y0 = y0;
// Get absolute x/y offset
let dx = if x0 > x1 { x0 - x1 } else { x1 - x0 };
let dy = if y0 > y1 { y0 - y1 } else { y1 - y0 };
// Get slopes
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
// Initialize error
let mut err = if dx > dy { dx } else {-dy} / 2;
let mut err2;
loop {
// Set pixel
img.get_pixel_mut(x0 as u32, y0 as u32).data = [255, 255, 255];
// Check end condition
if x0 == x1 && y0 == y1 { break };
// Store old error
err2 = 2 * err;
// Adjust error and start position
if err2 > -dx { err -= dy; x0 += sx; }
if err2 < dy { err += dx; y0 += sy; }
}
}
fn main() {
let mut img = RgbImage::new(256, 256);
draw_line(&mut img, 10, 10, 246, 128);
draw_line(&mut img, 128, 10, 10, 246);
img.save("output.png").unwrap();
}
Output:
As a primitive form of adding thickness, you could repeat drawing the line with some offset. Alternatively, draw a filled rectangle where the height of the rectangle corresponds to the thickness of the desired line.
There's a ticket open in the imageproc project to add anti aliased line drawing support: https://github.com/PistonDevelopers/imageproc/issues/97

Related

How can I draw a line while only using vertices and indices?

I use wgpu as my graphics backend for my project.
this is my current implementation:
pub fn draw_line(
points: Vec<[f32; 2]>,
) -> (Vec<Vertex>, Vec<u16>) {
let mut vertices: Vec<Vertex> = Vec::new();
let mut indices: Vec<u16> = Vec::new();
let w = WIDTH / 2.0;
let x1 = points[0][0];
let x2 = points[1][0];
let y1 = points[0][1];
let y2 = points[1][1];
let color: [f32; 3] = [1.0, 1.0, 1.0];
vertices.push(Vertex { position: [x1, y1 - w, 0.0], color });
vertices.push(Vertex { position: [x1, y1 + w, 0.0], color });
vertices.push(Vertex { position: [x2, y2 + w, 0.0], color });
vertices.push(Vertex { position: [x2, y2 - w, 0.0], color });
indices.push(2);
indices.push(1);
indices.push(0);
indices.push(2);
indices.push(0);
indices.push(3);
return (vertices, indices);
}
But when trying to draw a line between 2 points the width of the line gets distorted relative to the height difference of those points.
And the X and Y values of point1 must be smaller than the ones on point2 otherwise they dont show up because wgpu needs either Clockwise or CounterClockwise front faces
Is there any better function that that returns the vertices and indices, for a line between 2 Points
Untested but should work:
pub fn draw_line(
points: Vec<[f32; 2]>,
) -> (Vec<Vertex>, Vec<u16>) {
let mut vertices: Vec<Vertex> = Vec::new();
let mut indices: Vec<u16> = Vec::new();
let w = WIDTH / 2.0;
let x1 = points[0][0];
let x2 = points[1][0];
let y1 = points[0][1];
let y2 = points[1][1];
let color: [f32; 3] = [1.0, 1.0, 1.0];
let dx = x2 - x1;
let dy = y2 - y1;
let l = dx.hypot (dy);
let u = dx * WIDTH * 0.5 / l;
let v = dy * WIDTH * 0.5 / l;
vertices.push(Vertex { position: [x1 + v, y1 - u, 0.0], color });
vertices.push(Vertex { position: [x1 - v, y1 + u, 0.0], color });
vertices.push(Vertex { position: [x2 - v, y2 + u, 0.0], color });
vertices.push(Vertex { position: [x2 + v, y2 - u, 0.0], color });
indices.push(2);
indices.push(1);
indices.push(0);
indices.push(2);
indices.push(0);
indices.push(3);
return (vertices, indices);
}

How to draw this circle instead of Bresenham's Circle Algorithm

int main()
{
const auto console = ::GetConsoleWindow();
const auto context = ::GetDC(console);
constexpr auto red = RGB(255, 0, 0);
constexpr auto yellow = RGB(255, 255, 0);
RECT rectClient, rectWindow;
GetClientRect(console, &rectClient);
GetWindowRect(console, &rectWindow);
int posx, posy;
posx = GetSystemMetrics(SM_CXSCREEN) / 2 - (rectWindow.right - rectWindow.left) / 2;
posy = GetSystemMetrics(SM_CYSCREEN) / 2 - (rectWindow.bottom - rectWindow.top) / 2;
const int radius = 150;
for (int y = -radius; y <= radius; y++)
for (int x = -radius; x <= radius; x++)
if (x * x + y * y <= radius * radius)
SetPixel(context, posx + x, posy + y, red);
}
It gives me this result img
it looks good but i saw this weird pixels at sides (up, down, right, left)
img
and this is what I want (I added some pixels at the top so it looks better)
enter image description here
Your "what I want" looks anti-aliased. So draw anti-aliased.
If the original condition is not met, but x*x + y*y <= (radius+1)*(radius+1) is met then you need a partially-shaded pixel.
Another way to do anti-aliasing is to test not the center of each pixel but the four corners (x \plusminus 0.5, y \plusminus 0.5). If more than zero but fewer than four corners are inside the circle, you need a partially-shaded pixel.

Circle Rasterization Algorithm - center between pixels

I have a problem where I have to select all squares (think pixels) that are partially within a circle (even if the circle only cuts through a small corner of the square, but not if it goes through one of the corner vertices). The radius is an integer multiple of the pixel size.
The problem is that the center of the circle is between pixels, i.e. on the corner vertices of four pixels.
I want to visit each pixel only once.
For example, I would like to select all white pixels in the following images:
R = 8 px
R = 10 px
For a circle with the center in the center of a pixel, this wouldn't be a problem, and I could use the usual form of the Bresenham algorithm:
public void checkCircle(int x0, int y0, int radius) {
int x = radius;
int y = 0;
int err = -x;
while (x > 0) {
while (err <= 0) {
y++;
err += 2 * y + 1;
}
checkVLine(x0 + x, y0 - y, y0 + y);
checkVLine(x0 - x, y0 - y, y0 + y);
x--;
err -= 2 * x + 1;
}
checkVLine(x0, y0 - radius, y0 + radius);
}
public void checkVLine(int x, int y0, int y1) {
assert(y0 <= y1);
for (int y = y0; y <= y1; y++)
checkPixel(x, y);
}
Sadly, I don't see how to adapt it to support inter-pixel circles.
For the first quadrant - cell should be marked if its left bottom corner lies inside circle, so you can rasterize with simple loops
for dy = 0 to R-1
dx = 0
sq = R * R - dy * dy
while dx * dx < sq
mark (dx, dy)
mark (dx, -dy-1)
mark (-dx-1, dy)
mark (-dx-1, -dy-1)
To fill whole horizontal lines, you can calculate max value for dx
for dy = 0 to R-1
mdx = Floor(Sqrt(R * R - dy * dy))
fill line (-mdx-1,dy)-(mdx,dy)
fill line (-mdx-1,-dy-1)-(mdx,-dy-1)

How to connect 4 points on a plane so they do not fold on themselves(they always create a Quadrilateral)

I have created a small Raphael app to showcase my struggle.
I created four handles which can be moved. A 'sheet' is covering the entire screen except for the square between the 4 handles.
Whenever the handles are dragged the sheet is placed accordingly.
What ends up happening is that in certain situations, the sheet folds on itself.
It's best if you just see the fiddle. You'll get what I'm talking about
http://jsfiddle.net/8qtffq0s/
How can I avoid this?
Notice: The screen is white. The black part is the sheet, and the white part is a gap in the sheet and not the other way around.
//raphael object
var paper = Raphael(0, 0, 600, 600)
//create 4 handles
h1 = paper.circle(50, 50, 10).attr("fill","green")
h2 = paper.circle(300, 50, 10).attr("fill", "blue")
h3 = paper.circle(300, 300, 10).attr("fill", "yellow")
h4 = paper.circle(50, 300, 10).attr("fill", "red")
//create covering sheet
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", h1.attrs.cx, h1.attrs.cy,"L", h4.attrs.cx, h4.attrs.cy, h3.attrs.cx, h3.attrs.cy, h2.attrs.cx, h2.attrs.cy,'z']
sheet = paper.path(path).attr({ "fill": "black", "stroke": "white" }).toBack()
//keep starting position of each handle on dragStart
var startX,startY
function getPos(handle) {
startX= handle.attrs.cx
startY = handle.attrs.cy
}
//Redraw the sheet to match the new handle placing
function reDrawSheet() {
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", h1.attrs.cx, h1.attrs.cy, "L", h4.attrs.cx, h4.attrs.cy, h3.attrs.cx, h3.attrs.cy, h2.attrs.cx, h2.attrs.cy, 'z']
sheet.attr("path",path)
}
//enable handle dragging
h1.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h2.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h3.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h4.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
Update: I improved the function "reDrawSheet" so now it can classify the points on the strings as top left, bottom left, bottom right, and top right
This solved many of my problems, but in some cases the sheet still folds on it self.
new fiddle: http://jsfiddle.net/1kj06co4/
new code:
function reDrawSheet() {
//c stands for coordinates
c = [{ x: h1.attrs.cx, y: h1.attrs.cy }, { x: h4.attrs.cx, y: h4.attrs.cy }, { x: h3.attrs.cx, y: h3.attrs.cy }, { x: h2.attrs.cx, y: h2.attrs.cy }]
//arrange the 4 points by height
c.sort(function (a, b) {
return a.y - b.y
})
//keep top 2 points
cTop = [c[0], c[1]]
//arrange them from left to right
cTop.sort(function (a, b) {
return a.x - b.x
})
//keep bottom 2 points
cBottom = [c[2], c[3]]
//arrange them from left to right
cBottom.sort(function (a, b) {
return a.x - b.x
})
//top left most point
tl = cTop[0]
//bottom left most point
bl = cBottom[0]
//top right most point
tr = cTop[1]
//bottom right most point
br = cBottom[1]
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", tl.x,tl.y, "L", bl.x,bl.y, br.x,br.y, tr.x,tr.y, 'z']
sheet.attr("path",path)
}
To make things super clear, this is what I'm trying to avoid:
Update 2:
I was able to avoid the vertices from crossing by checking which path out of the three possible paths is the shortest and choosing it.
To do so, I added a function that checks the distance between two points
function distance(a, b) {
return Math.sqrt(Math.pow(b.x - a.x, 2) + (Math.pow(b.y - a.y, 2)))
}
And altered the code like so:
function reDrawSheet() {
//c stands for coordinates
c = [{ x: h1.attrs.cx, y: h1.attrs.cy }, { x: h4.attrs.cx, y: h4.attrs.cy }, { x: h3.attrs.cx, y: h3.attrs.cy }, { x: h2.attrs.cx, y: h2.attrs.cy }]
//d stands for distance
d=distance
//get the distance of all possible paths
d1 = d(c[0], c[1]) + d(c[1], c[2]) + d(c[2], c[3]) + d(c[3], c[0])
d2 = d(c[0], c[2]) + d(c[2], c[3]) + d(c[3], c[1]) + d(c[1], c[0])
d3 = d(c[0], c[2]) + d(c[2], c[1]) + d(c[1], c[3]) + d(c[3], c[0])
//choose the shortest distance
if (d1 <= Math.min(d2, d3)) {
tl = c[0]
bl = c[1]
br = c[2]
tr = c[3]
}
else if (d2 <= Math.min(d1, d3)) {
tl = c[0]
bl = c[2]
br = c[3]
tr = c[1]
}
else if (d3 <= Math.min(d1, d2)) {
tl = c[0]
bl = c[2]
br = c[1]
tr = c[3]
}
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", tl.x,tl.y, "L", bl.x,bl.y, br.x,br.y, tr.x,tr.y, 'z']
sheet.attr("path",path)
}
Now the line does not cross itself like the image I attached about, but the sheet "flips" so everything turns black.
You can see the path is drawn correctly to connect the for points by the white stroke, but it does not leave a gap
new fiddle: http://jsfiddle.net/1kj06co4/1/
Picture of problem:
So... the trouble is to tell the inside from the outside.
You need the following functions:
function sub(a, b) {
return { x: a.x - b.x , y: a.y - b.y };
}
function neg(a) {
return { x: -a.x , y: -a.y };
}
function cross_prod(a, b) {
// 2D vecs, so z==0.
// Therefore, x and y components are 0.
// Return the only important result, z.
return (a.x*b.y - a.y*b.x);
}
And then you need to do the following once you've found tl,tr,br, and bl:
tlr = sub(tr,tl);
tbl = sub(bl,tl);
brl = sub(bl,br);
btr = sub(tr,br);
cropTL = cross_prod( tbl, tlr );
cropTR = cross_prod(neg(tlr),neg(btr));
cropBR = cross_prod( btr, brl );
cropBL = cross_prod(neg(brl),neg(tbl));
cwTL = cropTL > 0;
cwTR = cropTR > 0;
cwBR = cropBR > 0;
cwBL = cropBL > 0;
if (cwTL) {
tmp = tr;
tr = bl;
bl = tmp;
}
if (cwTR == cwBR && cwBR == cwBL && cwTR!= cwTL) {
tmp = tr;
tr = bl;
bl = tmp;
}
My version of the fiddle is here. :) http://jsfiddle.net/1kj06co4/39/

Problems limiting object rotation with Mathf.Clamp()

I am working on a game that rotates an object on the z axis. I need to limit the total rotation to 80 degrees. I tried the following code, but it doesn't work. minAngle = -40.0f and maxAngle = 40.0f
Vector3 pos = transform.position;
pos.z = Mathf.Clamp(pos.z, minAngle, maxAngle);
transform.position = pos;
The code you posted clamps the z position. What you want is to use transform.rotation
void ClampRotation(float minAngle, float maxAngle, float clampAroundAngle = 0)
{
//clampAroundAngle is the angle you want the clamp to originate from
//For example a value of 90, with a min=-45 and max=45, will let the angle go 45 degrees away from 90
//Adjust to make 0 be right side up
clampAroundAngle += 180;
//Get the angle of the z axis and rotate it up side down
float z = transform.rotation.eulerAngles.z - clampAroundAngle;
z = WrapAngle(z);
//Move range to [-180, 180]
z -= 180;
//Clamp to desired range
z = Mathf.Clamp(z, minAngle, maxAngle);
//Move range back to [0, 360]
z += 180;
//Set the angle back to the transform and rotate it back to right side up
transform.rotation = Quaternion.Euler(transform.rotation.eulerAngles.x, transform.rotation.eulerAngles.y, z + clampAroundAngle);
}
//Make sure angle is within 0,360 range
float WrapAngle(float angle)
{
//If its negative rotate until its positive
while (angle < 0)
angle += 360;
//If its to positive rotate until within range
return Mathf.Repeat(angle, 360);
}
Here's a static version of the nice solution by Imapler that, instead of changing the angle itself, it returns the campled angle, so it can be used with any axis.
public static float ClampAngle(
float currentValue,
float minAngle,
float maxAngle,
float clampAroundAngle = 0
) {
return Mathf.Clamp(
WrapAngle(currentValue - (clampAroundAngle + 180)) - 180,
minAngle,
maxAngle
) + 360 + clampAroundAngle;
}
public static float WrapAngle(float angle)
{
while (angle < 0) {
angle += 360;
}
return Mathf.Repeat(angle, 360);
}
Or if you don't expect to use the WrapAngle method, here's an all-in-one version:
public static float ClampAngle(
float currentValue,
float minAngle,
float maxAngle,
float clampAroundAngle = 0
) {
float angle = currentValue - (clampAroundAngle + 180);
while (angle < 0) {
angle += 360;
}
angle = Mathf.Repeat(angle, 360);
return Mathf.Clamp(
angle - 180,
minAngle,
maxAngle
) + 360 + clampAroundAngle;
}
So now you can do:
transform.localEulerAngles.x = YourMathf.ClampAngle(
transform.localEulerAngles.x,
minX,
maxX
);

Resources