Drawing varying line widths with NSBezierPath? - cocoa

I would like to create a vector object that can be drawn with a pen tablet and will honor the pressure information from the pen (by increasing/decreasing line width appropriately). Now I know how to get the pressure info out of the NSEvent, but of course NSBezierPath doesn't support varying line widths.
So I'm trying to generate a Bezier path that is the outline of my line, by calculating the perpendicular lines at the ends of the curve and connecting those with the very same curve. It nearly works in my little test project for a single curve segment:
https://github.com/uliwitness/WideningBezierPathTest
but this has one big issue: The line thins to barely 2 pixels in the middle (it should only go from 32 to 8). Does anyone know how I'd adjust the control points so the two edges of the line in the curved part are at roughly the same distance ?
Anyone have ideas? Suggestions? Useful articles?

Thanks to suggestions from others I've managed to cobble something together that works. I've committed the changes to the repository, but you can go back a few revisions if you want to see the older code. Here's what I do now:
I flatten the path using bezierPathByFlatteningPath. This gives me straight line segments.
I then calculate the start and end points of the perpendicular lines at the start and end of each line segment (these lines are as long as the line size should be at this point).
I create a bezier path containing a parallelogram consisting of the two perpendicular lines plus lines connecting them. This gives a smooth transition in line widths for each segment.
Once I have the segments, I draw the ending perpendicular lines of the path, plus the sides of each segment (but not the perpendicular lines between segments) into a new bezier path, which I can then fill to draw the stroke with the desired line width variations.
If you want to use this for printing or under HiDPI, you might have to muck with the "flatness" of the bezier path, but for 1x screen display it looks fine.

Related

Timing issue on multi-line chart?

I am working on a multi-line animated chart based on [Edit: not Bostock's] example:
http://bl.ocks.org/atmccann/8966400
I want the lines to appear synchronized by the date value on the x axis. If I slow down the graph (say to duration=15000) I can clearly see that in the very early part of the graph the green line is ahead of the black line. Then the black line moves ahead of the green line. Both start and end together.
The issue is accentuated in my chart where I am plotting 20 lines. Some are clearly ahead of the others when plotting. Lines with initial y values of zero shoot way ahead of those that climb to higher y values at the start.
How can I adjust the display so both lines paint for the same date at the same time?
The technique in the bl.ocks.org example (which, btw, is not Bostock's) is animating based on the length of the line, not it's position on the y-axis. If you want to animate based on the y-axis position, you'll have to use a different technique. Especially with 20 lines, I wouldn't recommend trying to dynamically update each line if they have a lot of points. You could do that if there aren't many points, though.
A simple approach would be to add a solid (white) rectangle to the chart that covers all the graph lines. Then animate the left position and width of that rectangle to reveal the lines over time.

Need line drawing algorithm for simulating natural pencil

I'm writing a drawing program that uses a pressure sensitive table for input. I'd like to be able to simulate the soft pencil effect that many other art programs have (such as Paint Tool SAI, Art Rage). Technique I'm using at the moment is functional, but is missing the cleanness I see in more professional programs.
My algorithm at the moment works like this:
Create a bitmap representing the head of the brush. This is just a transparent bitmap with a black circle drawn on it. The circle has an inner radius that is solid black and an outer radius. The blackness linearly fades from opaque to transparent as you move from the inner to the outer radius.
Capture input events from my tablet. Each point contains an (x, y) coordinate as well as a pressure value
For every point after the first one, draw a line from the previous point to the current one. This is done by drawing (daubing) the brush bitmap several times between the two points. The step size between each daub is chosen so there is an overlap between subsequent daubs.
This works reasonably well, but the result is a line that is somewhat blobby and jagged.
One thing I need to do is somehow smooth out the input points so that the stroke as a whole is smooth.
The other thing I need to do is figure out how to 'drag' the brush head along this path to make the stroke. If the spacing is too far apart, the stroke looks like a line of circles. If too close together, the stroke builds up on itself and becomes very dark. (I tried to fix this by attenuating the brush by the spacing. This does make things more consistent, but stops the stroke from being fully opaque).
Anyhow, I'd expect that there's a lot of research already done on this, if only I knew where to look. Please let me know if there are any better pencil drawing algorithms out there.
Instead of drawing the new circle over what has already been drawn, using the standard blending functions (so that regions of overlap get a higher opacity), you need to keep the maximum opacity so far.
Only after you have built up the complete stroke (as on a white sheet), you can blend it to the existing line art.
The picture illustrates the difference between blending and keeping the maximum opacity.

How to get a 1 pixel line with NSBezierPath?

I'm developing a custom control. One of the requirements is to draw lines. Although this works, I noticed that my 1 pixel wide lines do not really look like 1 pixel wide lines - I know, they're not really pixels but you know what I mean. They look more like two or three pixels wide. This becomes very apparent when I draw a dashed line with a 1 pixel dash and a 2 pixel gap. The 1 pixel dashes actually look like tiny lines in stead of dots.
I've read the Cocoa Drawing documentation and although Apple mentions the setLineWidth method, changing the line width to values smaller than 1.0 will only make the line look more vague and not thinner.
So, I suspect there's something else influencing the way my lines look.
Any ideas?
Bezier paths are drawn centered on their path, so if you draw a 1 pixel wide path along the X-coordinate, the line actually draws along Y-coordinates { -0.5, 0.5 } The solution is usually to offset the coordinate by 0.5 so that the line is not drawn in the sub pixel boundaries. You should be able to shift your bounding box by 0.5 to get sharper drawing behavior.
Francis McGrew already gave the right answer, but since I did a presentation on this once, I thought I'd add some pictures.
The problem here is that coordinates in Quartz lie at the intersections between pixels. This is fine when filling a rectangle, because every pixel that lies inside the coordinates gets filled. But lines are technically (mathematically!) invisible. To draw them, Quartz has to actually draw a rectangle with the given line width. This rectangle is centered over the coordinates:
So when you ask Quartz to stroke a rectangle with integral coordinates, it has the problem that it can only draw whole pixels. But here you see that we have half pixels. So what it does is it averages the color. For a 50% black (the line color) and 50% white (the background) line, it simply draws each pixel in grey:
This is where your washed-out drawings come from. The fix is now obvious: Don't draw between pixels, and you achieve that by moving your points by half a pixel, so your coordinate is centered over the desired pixel:
Now of course just offsetting may not be what you wanted. Because if you compare the filled variant to the stroked one, the stroke is one pixel larger towards the lower right. If you're e.g. clipping to the rectangle, this will cut off the lower right:
Since people usually expect the rectangle to stroke inside the specified rectangle, what you usually do is that you offset by 0.5 towards the center, so the lower right effectively moves up one pixel. Alternately, many drawing apps offset by 0.5 away from the center, to avoid overlap between the border and the fill (which can look odd when you're drawing with transparency).
Note that this only holds true for 1x screens. 2x Retina screens actually exhibit this problem differently, because each of the pixels below is actually drawn by 4 Retina pixels, which means they can actually draw the half-pixels. However, you still have the same problem if you want a sharp 0.5pt line. Also, since Apple may in the future introduce other Retina screens where e.g. every pixel is made up of 9 Retina pixels (3x), or whatever, you should really not rely on this. Instead, there are now API calls to convert rectangles to "backing aligned", which does this for you, no matter whether you're running 1x, 2x, or a fictitious 3x.
PS - Since I went to the hassle of writing this all up, I've put this up on my web site: http://orangejuiceliberationfront.com/are-your-rectangles-blurry-pale-and-have-rounded-corners/ where I'll update and revise this description and add more images.
The answer is (buried) in the Apple Docs:
"To avoid antialiasing when you draw a one-point-wide horizontal or vertical line, if the line is an odd number of pixels in width, you must offset the position by 0.5 points to either side of a whole-numbered position"
Hidden in Drawing and Printing Guide for iOS: iOS Drawing Concepts, though nothing that specific to be found in the current, standard (OS X) Cocoa Drawing Guide..
As for the effects of invoking setDefaultLineWidth: the docs also state that:
"A width of 0 is interpreted as the thinnest line that can be rendered on a particular device. The actual rendered line width may vary from the specified width by as much as 2 device pixels, depending on the position of the line with respect to the pixel grid and the current anti-aliasing settings. The width of the line may also be affected by scaling factors specified in the current transformation matrix of the active graphics context."
I found some info suggesting that this is caused by anti aliasing. Turning anti aliasing off temporarily is easy:
[[NSGraphicsContext currentContext] setShouldAntialias: NO];
This gives a crisp, 1 pixel line. After drawing just switch it on again.
I tried the solution suggested by Francis McGrew by offsetting the x coordinate with 0.5, however that did not make any difference to the appearance of my line.
EDIT:
To be more specific, I changed x and y coordinates individually and together with an offset of 0.5.
EDIT 2:
I must have done something wrong, as changing the coordinates with an offset of 0.5 actually does work. The end result is better than the one obtained by switching off the anti aliasing so I'll make Francis MsGrew's answer the accepted answer.

Drawing Stippled Lines using OpenGL-ES

I have an application that draws 3-d map view marked up lines that show various features.
I am porting the map over to an OpenGL-ES architecture, but am having a bit of trouble working out how to display dashed lines.
Doing a lot of googling, I've found many references to the idea that drawing dashed lines and polygons were removed from OpenGL-ES as they can be easily emulated using textures triangles. That's great, but I can't find anyone that actually does this emulation and/or has a description of the steps involved.
An example, of one problem I have encountered trying to prototype out this concept is perspective squeezes my lines to invisible as they go towards the horizon. Using LINE_STRIP, this doesn't happen, and the lines remain a constant width in the map.
Any advice on how to achieve dashed constant width lines in a perspective view would be much appreciated.
I believe you can apply a texture to a line, not just a triangle. You'll need to set texture coordinates for each end of the line; the texture will be interpolated linearly along the line.
The effectiveness of this solution should be invariant of whether you use lines or line strips - line strips are just a way to create lines with fewer vertices.
There is one other problem: the tendency of the texture pattern to become compact as a line goes away from the camera. This happens because texture coordinate interpolation is perspective-correct even for lines (see section 3.5 of the GL spec).
There are two ways to get around this:
If you can calculate a "q" coordinate for your texture that undoes the perspective, you can restore screen-space texturing. This technique is probably too performance-expensive.
You can project the texture in eye space (e.g. glTexGen).
Texture coordinate generation is of course not available in GLES 1.1, but if you are using vertices by array, you can fake it by:
Setting your texture coordinate array to be your vertex coordinate array and
Using the texture matrix to "transform" the vertices.
The disadvantage of this technique is that the texture pattern will be in fixed screen space - that is, the texture won't run across the lines.
If all you want is to draw dashed lines, just change from GL_LINE_STRIP to GL_LINES. That way, open GL will connect vertices 1&2, 3&4, 5&6, but not 3&4, or 4&5 leaving spaces there. It will in essence be a half/half ratio dotted line-- the rough equivalent of glLineStipple(1, 0101);
IE: in the vertex array
[0,0,1,1,2,2,3,3,4,4,5,5,6,6]
OpenGL will connect (0,0) to (1,1), but will not connect (1,1) to (2,2) [whereas it would with GL_LINE_STRIP]. It will then connect (3,3) to (4,4), but NOT (4,4) to (5,5). The final connection will be (5,5) to (6,6).
This is what it looks like when I did it:
Dotted Lines on Android
The line is not 50/50 dotted/empty because in my case, it represents the position of a game entity each game frame-- and game frames are not necessarily all of equal length, thus the inconsistent line:space ratio.
The code looks like this to draw it:
public void draw(GL10 gl)
{
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glColor4f(color[0], color[1], color[2], color[3] / fade);
//pointsBuffer holds the points on the line
gl.glVertexPointer(2, GL10.GL_FLOAT, 0, pointsBuffer);
gl.glDrawArrays(GL10.GL_LINES, 0, points.length/2);
gl.glEnable(GL10.GL_TEXTURE_2D);
}
An alternative idea to create a more intentionally patterned stipple would be to skip certain vertices by instead drawing with an indices array (glDrawElements). However, I don't have an example of that to show you. :/

Bresenham line algorithm (thickness)

I was wondering if anyone knew of any algorithm to draw a line with specific thickness, based on Bresenham's line algorithm or any similar.
On a second thought, I've been wondering about for each setPixel(x,y) I'd just draw a circle, e.g.:
filledCircle(x,y,thickness); for every x,y but that would of course be very slow. I also tried to use dictionary but that would fill the memory in no time. Check the pixels I'm about to draw on if they have the same color, but that's also not efficient enough for large brushes.
Perhaps I could somehow draw half circles depending on the angle?
Any input would be appreciated.
Thanks.
duplicate: how do I create a line of arbitrary thickness using Bresenham?
You cannot actually draw circles along the line. This approach is patented. :)
You can still read patent for inspiration.
I don't know what is commonly used, but it seems to me that you could use Bresenham for the 1-pixel-wide line, but extend it a set number of pixels vertically or horizonally. For instance, suppose your line is roughly 30 degrees away from the horizontal, and you want it to be four pixels wide. You calculate that the vertical thickness of the line should be five pixels. You run Bresenham, but for each pixel (x,y), you actually draw (x,y), (x,y+1), ... (x,y+4). And if you want the ends of the line to be rounded, draw a circle at each end.
For overkill, make a pixel map of the stylus (a circle or diagonal nib, or whatever), then draw a set of parallel Bresenham lines, one for each pixel in the stylus.
There are variations on Bresenhams which calculate pixel coverage, such as those used in the anti-grain geometry libraries; whether you want something that quality - you don't say what the output medium is, and most systems more capable than on-off LCDS support pens with thickness anyway.

Resources