Many graphics packages allow the user to select where they would like to draw the border of a region around a shape; either along the inside, outside or centre of the shape. For example, this shows the same square with the border drawn along the centre, inside and outside respectively:
I could scale the path up/down based on the stroke's width, but I wanted to check if there was built-in support for this first.
I'm using Ruby, but if there's a C method for this, it's likely available in the Ruby bindings as well.
Is there a method to draw a stroke around the outside or inside of a path, rather than along the centre, in Cairo?
No, there is no such method built-in.
One could likely approximate this with a temporary surface that is later used as a mask. For example, to do "outside", you first fill a temporary surface with "transparent", then stroke with twice your desired line width some "opaque", and finally fill the shape with "transparent" to get rid of the inner part of the line width. The resulting surface can then be used as a mask.
"Inside" would be similar, but with an extra trick: Again, transparent surface and stroke with twice the line width. Now the outside part of this stroke needs to be removed. For this, one needs a path with a winding rule of even-odd. Add a surface-sized rectangle to this path inverts the path, thus allowing to remove everything outside via a fill.
For a non-zero winding rule... I do not have any immediate ideas (well, another temporary surface that is then inverted via a full-surface-paint with operator SUBTRACT?).
Sample code for drawing outside of the path (see comments):
static void draw_outside_of_path(cairo_t *cr) {
double line_width = cairo_get_line_width(cr);
cairo_pattern_t *mask;
cairo_push_group_with_content(cr, CAIRO_CONTENT_ALPHA);
cairo_set_line_width(cr, 2 * line_width);
cairo_set_source_rgba(cr, 0, 0, 0, 1);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_stroke_preserve(cr);
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
cairo_fill_preserve(cr);
mask = cairo_pop_group(cr);
cairo_mask(cr, mask);
cairo_pattern_destroy(mask);
}
For stroking inside a path, set the path as a clipping region, then stroke the path; any part of stroke that lies outside the clipping region will be unseen...
Related
I'm trying to understand Cairo framework; but I don't know how to put more than one shape into the drawing area. All codes/tutorials I've seen are either too advanced or do not relate to this issue.
This is my code to draw a circle (I'm using C and gtk+3.0):
void draw(GtkWidget *this, cairo_t *cr, gpointer data) {
cairo_set_line_width(cr, 5);
cairo_set_source_rgb(cr, 0, 0, 0);
cairo_translate(cr, prog.width/2, prog.height/2);
cairo_arc(cr, 0, 0, 50, 0, 2 * M_PI);
cairo_stroke_preserve(cr);
cairo_set_source_rgb(cr, 0.9, 0.9, 0.9);
cairo_fill(cr);
}
I understand all but one thing: cairo_t *cr. From what I've been searching cr is a Context, but didn't quite grasped an intuitive idea of what it is about (maybe my question lies on this understanding?).
So, if I needed more circles near this one, how could I create them?
My try was using cairo_move_to(cr, x, y) and draw another shape but obviously it didn't work.
Thanks!
Yes, cairo_t is the type of a cairo context.
To draw on cairo, you set drawing parameters, a source which defines the color or image that gets drawn, and a path that specifies the shape that gets drawn, and then you call cairo_stroke() or cairo_fill() to do the actual drawing. After calling those, the path is reset (unless you use the _preserve versions of the functions), but everything else stays the same.
So to draw again, you just need to add more setup and drawing function calls after the first one.
cairo_move_to() does not actually move anything. What cairo_move_to() does is change the position of the "current point" of the path. Path components you add later, such as cairo_line_to(), will start at the current point and then set the current point to their end point.
In your case, you can draw multiple circles by adding a cairo_move_to() after the last line of your draw handler and then repeating the steps you used to draw the first circle.
cairo_arc() is different because you specify the center of the arc as the second and third arguments. To draw an arc somewhere else, you need to change those two arguments. (The current point does play a role in cairo_arc(); you'll need to see the documentation for information.)
The cairo documentation is the best place to start learning about cairo; it has lots of tutorials and samples.
I have a Path that normally has a StrokeThickness of 1. Under certain circumstances, it needs to have a StrokeThickness of 10. When I increase the stroke thickness, I don't want the path to take any additional space.
By default, just increasing the StrokeThickness increases the rendered size of the path. So you get something like this (the blue outline extends beyond the black boundary):
This is what I'm trying to achieve (the blue outline stays within the black boundary):
I can think of two mathematical ways to compensate for the increased StrokeWidth:
Manually adjust the points of the triangle inward.
Use a ScaleTransform on the Geometry of the Path.
Both of these would be somewhat problematic/complex. Is there an easier way?
You could clip the path by its own geometry like this:
<Path ... Clip="{Binding Data, RelativeSource={RelativeSource Self}}"/>
but then you would need to double the StrokeThickness, since only half of the stroke is visible.
On a whim I set StrokeThickness = -1 on my Rectangle and it did exactly what I wanted it to: the stroke goes on the inside of the Rectangle rather than on the outside.
I'm trying to do simple drawing in a subclass of a decorator, similar to what they're doing here...
How can I draw a border with squared corners in wpf?
...except with a single-pixel border thickness instead of the two they're using there. However, no matter what I do, WPF decides it needs to do its 'smoothing' (e.g. instead of rendering a single-pixel line, it renders a two-pixel line with each 'half' about 50% of the opacity.) In other words, it's trying to anti-alias the drawing. I do not want anti-aliased drawing. I want to say if I draw a line from 0,0 to 10,0 that I get a single-pixel-wide line that's exactly 10 pixels long without smoothing.
Now I know WPF does that, but I thought that's specifically why they introduced SnapsToDevicePixels and UseLayoutRounding, both of which I've set to 'True' in the XAML. I'm also making sure that the numbers I'm using are actual integers and not fractional numbers, but still I'm not getting the nice, crisp, one-pixel-wide lines I'm hoping for.
Help!!!
Mark
Aaaaah.... got it! WPF considers a line from 0,0 to 10,0 to literally be on that logical line, not the row of pixels as it is in GDI. To better explain, think of the coordinates in WPF being representative of the lines drawn on a piece of graph paper whereas the pixels are the squares those lines make up (assuming 96 DPI that is. You'd need to adjust accordingly if they are different.)
So... to get the drawing to refer to the pixel locations, we need to shift the drawing from the lines themselves to be the center of the pixels (squares on graph paper) so we shift all drawing by 0.5, 0.5 (again, assuming a DPI of 96)
So if it is a 96 DPI setting, simply adding this in the OnRender method worked like a charm...
drawingContext.PushTransform(new TranslateTransform(.5, .5));
Hope this helps others!
M
Have a look at this article: Draw lines exactly on physical device pixels
UPD
Some valuable quotes from the link:
The reason why the lines appear blurry, is that our points are center
points of the lines not edges. With a pen width of 1 the edges are
drawn excactly between two pixels.
A first approach is to round each point to an integer value (snap to a
logical pixel) an give it an offset of half the pen width. This
ensures, that the edges of the line align with logical pixels.
Fortunately the developers of the milcore (MIL stands for media
integration layer, that's WPFs rendering engine) give us a way to
guide the rendering engine to align a logical coordinate excatly on a
physical device pixels. To achieve this, we need to create a
GuidelineSet
protected override void OnRender(DrawingContext drawingContext)
{
Pen pen = new Pen(Brushes.Black, 1);
Rect rect = new Rect(20,20, 50, 60);
double halfPenWidth = pen.Thickness / 2;
// Create a guidelines set
GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(rect.Left + halfPenWidth);
guidelines.GuidelinesX.Add(rect.Right + halfPenWidth);
guidelines.GuidelinesY.Add(rect.Top + halfPenWidth);
guidelines.GuidelinesY.Add(rect.Bottom + halfPenWidth);
drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawRectangle(null, pen, rect);
drawingContext.Pop();
}
Is there an automatic way to get all the points of an ellipse stroke, without the filling points?
In WPF there are no actual "Points" in a geometry - it is infinitely smooth. This can be seen by zooming in on an ellipse. You can go to 1,000,000x zoom and you can still see curvature and no points.
Since WPF shapes aren't composed of points, your question can be interepted in several ways. You may be looking for any of these:
A list of points that approximates the boundary of the ellipse (polyline approximation)
A set of pixels covered by the ellipse including the fill
A set of pixels covered by the edge of the ellipse
Here are the solutions in each case:
If you're looking for an approximation of the ellipse as discrete points (ie. a dotted-line version that looks like an ellipse), use this code:
PolyLineSegment segment =
ellipse.DefiningGeometry
.GetFlattenedPathGeometry(1.0, ToleranceType.Absolute)
.Figures[0].Segments[0] as PolyLineSegment;
foreach(Point p in segment.Points)
...
If you're looking for the pixels affected, you'll need to RenderTargetBitmap:
RenderTargetBitmap rtb =
new RenderTargetBitmap(width, height, 96, 96, PixelFormat.Gray8);
rtb.Render(ellipse);
byte[] pixels = new byte[width*height];
rtb.CopyPixels(pixels, width, 0);
Any nonzero value in pixels[] is partially covered by the ellipse. This will include points interior to the ellipse if the ellipse has a fill.
If you need to get only the pixels along the edge but your ellipse is filled, or vice versa, you can create a new Shape to pass to RenderTargetBitmap:
var newEllipse = new Path
{
Data = ellipse.DefiningGeometry,
Stroke = Brushes.Black,
};
RenderTargetBitmap rtb = ...
[same as before]
By using Reflector I found out that there is a GetPointList() method in the EllipseGeometry class unfortunately it's private. Maybe you can invoke it through reflection but that's sounds like a very bad hack... I'll see if I find another way...
I've drawn an ellipse in the XZ plane, and set my perspective slightly up on the Y-axis and back on the Z, looking at the center of ellipse from a 45-degree angle, using gluPerspective() to set my viewing frustrum.
Unrotated, the major axis of the ellipse spans the width of my viewport. When I rotate 90-degrees about my line-of-sight, the major axis of the ellipse now spans the height of my viewport, thus deforming the ellipse (in this case, making it appear less eccentric).
What do I need to do to prevent this deformation (or at least account for it), so rotation about the line-of-sight preserves the perceived major axis of the ellipse (in this case, causing it to go beyond the viewport)?
It looks like you're using 1.0 as the aspect when you call gluPerspective(). You should use width/height. For example, if your viewport is 640x480, you would use 1.33333 as the aspect argument.
According to the OpenGL Spec:
void gluPerspective( GLdouble fovy,
GLdouble aspect,
GLdouble zNear,
GLdouble zFar )
Aspect should be a function of your window width and height. Specifically width divided by height (but watch out for division by zero).
Perhaps you are using 1 as the aspect which is not accurate unless your window is a square.
It looks like the aspect parameter on your gluPerspective call need tweaking. See The Man Page. If your window were physically square, the aspect ratio would be 1 and your problem would go away. However, your window is rectangular, so the viewing frustum needs to be non-square.
Set the aspect ratio to window_width / window_height, and your ellipse should look correct. Note that you'll need to update this whenever the window resizes; if you're using GLUT set a glutReshapeFunc and recalculate the projection matrix in there.