A deeper bevel on lineJoin()?



  • I'm in the process of animating diagrams to show the design rules behind Wim Crouwel's New Alphabet. The rules themselves can be explained with lines and points; the weight of the strokes and the shape of the joins are stylizations. But to show the rules being used to make Crouwel's letterforms it would be good to recreate what Crouwel showed in his specimens, and I'm running into a problem:

    newPage(500, 500)
    fill(1)
    rect(0, 0, 500, 500)
    translate(250, 250)
    path = BezierPath()
    path.moveTo((50, -50))
    path.lineTo((50, 50))
    path.lineTo((-50, 50))
    stroke(0)
    strokeWidth(50)
    lineJoin('bevel')
    lineCap('square')
    drawPath(path)
    
    saveImage('~/Desktop/crouwel_test.jpg')
    

    gives me:

    0_1526986737475_crouwel_test.jpg

    which is not as deep a bevel as Crouwel used—it essentially makes it look as if Crouwel folded ribbons at 90deg to make his letterforms:

    0_1526986881631_Screen Shot 2018-05-22 at 06.59.48.png

    I've tried setting the bevel at a negative number, but no luck. Any ideas?



  • And, while I'm asking questions: the code I'm writing is drawing segments in a single path. Is there a way to join these segments into a single, sometimes forking, path, where the contiguous parts are joined and capped per lineJoin and lineCap? Here's the code I have so far to sketch out Crouwel's basic structure:

    canvas = 720
    cellX = canvas/6
    cellY = canvas/6
    width = cellX/6 * 2
    
    nodes = [
        (-cellX, cellY * 2),
        (cellX, cellY * 2),
        (-cellX, cellY),
        (0, cellY),
        (cellX, cellY),
        (-cellX, 0),
        (cellX, 0),
        (-cellX, -cellY),
        (0, -cellY),
        (cellX, -cellY),
        (-cellX, -cellY * 2),
        (cellX, -cellY * 2)]
        
    segs = [
        [0, 0],
        [0, 1],
        [0, 2],
        [1, 1],
        [1, 4],
        [2, 3],
        [2, 4],
        [2, 5],
        [3, 4],
        [3, 8],
        [4, 6],
        [5, 6],
        [5, 7],
        [6, 9],
        [7, 8],
        [7, 9],
        [7, 10],
        [8, 9],
        [9, 11],
        [10, 10],
        [10, 11],
        [11, 11]
        ]
    
    for r in range(0, 512):
        newPage(canvas, canvas)
        fill(1)
        rect(0, 0, canvas, canvas)
        translate(canvas/2, canvas/2)
        count = bin(r)[2:]
        count = count.zfill(22)
        count = count[::-1]
        for n in range(len(nodes)):
            fill(1, 0, 0)
            oval(nodes[n][0] - 5, nodes[n][1] - 5, 10, 10)
        path = BezierPath()
        fill(None)
        strokeWidth(width)
        stroke(0)
        lineJoin('bevel')
        lineCap('square')
        glyph = []
        for d in range(len(count)):
            if count[d] == '1':
                path.moveTo(nodes[segs[d][0]])
                path.lineTo(nodes[segs[d][1]])
            path.closePath()
        drawPath(path)
    

    and here's an example of what it produces that I'm trying to avoid (the red circles show node locations):
    0_1527017188917_Screen Shot 2018-05-22 at 15.24.17.png


  • admin

    mm, you cannot set the 'dept' of the bevel.

    I would encourage you to have a look at the outline pen from the outliner extension

    To combine several lines its best to draw them together in a BezierPath



  • Frederik, I'll take a look. I wonder if this and the feature requests I just made could be covered if I just learned more about how to use pens … I really don't want DrawBot to wind up like Illustrator, I swear!



  • I was working on a fold-font system a few years ago. it took some time to get the trigonometry right. but once that was solved the possibilities were endless ...
    so the new alphabet could be rendered with different stroke weights
    0_1527183249831_light.png
    0_1527183268944_bold.png
    scaling in the x
    0_1527183207853_wide.png
    or y direction
    0_1527183234269_narrow.png
    and there could be multi-liners
    0_1527183160167_multilined.png

    seems like i did not compensate for the stroke weight at the end of a stroke but the system could be used for any collection of xy-points

    0_1527183141251_amp.png

    it is all a bit messy but let me know if you need some bits.


  • admin

    looks awesome!



  • @jo I'd love to see your code! Like I said, the way Crouwel used the corners was unrelated to the rules he used to construct the letterforms, so I could illustrate his structure without the beveled corners. But it would be nice in a presentation to show audiences the letters as he styled them.



  • @frederik Are all the import dependencies/paths in the outliner extension only going to make sense if I have RoboFont installed? Because I don't have RoboFont …



  • @mauricemeilleur I don’t know how Crouwel defined his rules. but to me the corner-rule seems like an integral part of the design.

    I had a look at my code and tried to tidy the worst bits. some of the angle calculations seem very inefficient but ¯\(ツ)/¯ here it is:

    EDIT 2018-05-28 changed the code to simplify some of the horrible trigonometry and there now is the sq_cap bool to have squared linecaps.

    # Draw a 'folded outline' between a set of points. 
    
    # TO DO: There should be an error message when two dots are closer to each other and the weight.
    # There should be a warning if the folding is further than the next points. 
    # There should be a warning for collinear points. 
    
    import math
    
    # Just a bunch of points
    
    p01 = (894, 334)
    p02 = (808, 86)
    p03 = (274, 792)
    p04 = (481, 920)
    p05 = (583, 730)
    p06 = (85, 430)
    p07 = (318, 100)
    p08 = (870, 600)
    p09 = (720, 690)
    
    points = [p01, p02, p03, p04, p05, p06, p07, p08, p09]
    
    
    def get_dist(p1, p2): return sqrt( (p2[0] - p1[0] )**2 + (p2[1] - p1[1])**2 )
    
    def calc_angle(p1, p2): return atan2((p2[1] - p1[1]), (p2[0] - p1[0]))
    
    def calc_delta(p1, p2, p3): 
        ''' 
        Returns the angle between three points.
        '''
        return  calc_angle(p2, p3) - calc_angle(p2, p1)
        
    
    
    def calc_offset(weight, p1, p2, p3):
        '''
        Returns the x and y "offset" to be added to the points of the polygon. 
        '''
        halfWeight = weight/2
    
        if p1 == p2:
            alpha = calc_angle(p1, p3)
            b = -sin(alpha) * halfWeight
            a =  cos(alpha) * halfWeight
        elif p2 == p3:
            alpha = calc_angle(p1, p3)
            b =  sin(alpha) * halfWeight
            a = -cos(alpha) * halfWeight
        else:
            alpha = calc_angle(p2, p3)
            delta = calc_delta(p1, p2, p3)
            xx = halfWeight / sin((delta - pi) / 2)
            delta_f = (2 * alpha - pi - delta ) / 2
    
            a, b = xx * sin(delta_f), xx * cos(delta_f)
        return a, b
    
    
    def wrap_line(p1, p2, p3, p4, weight):
        '''
        This draws a polygon around a line. Four points (x,y) and a weight parameter are required. 
        The polygon will be drawn around the two middle points.
        The first and last points define the angles.
        '''
        a, b = calc_offset(weight, p1, p2, p3)
        # extend first point
        x1b, y1b = p2[0] - b, p2[1] - a
        x1c, y1c = p2[0] + b, p2[1] + a
        # extend second point
        c, d = calc_offset(weight, p2, p3, p4)
        x2b, y2b = p3[0] - d, p3[1] - c
        x2c, y2c = p3[0] + d, p3[1] + c
    
        polygon((x1b, y1b), (x1c, y1c), (x2b, y2b), (x2c, y2c))
    
    
    
    # --------------------------------------------------------------------
    # The actual drawing
    
    fill(0, .4)
    
    weight = 180
    sq_cap = True
    
    if len(points) == 1:
        rect(points[0][0] - weight/2, points[0][1] - weight/2, weight, weight)    
    
    for i, point in enumerate(points[:-1]):
        # print (get_dist(point, points[i+1]))
    
        if len(points) == 2:
            wrap_line(points[i], points[i], points[i+1], points[i+1], weight)
        else:
            if i == 0:
                aaa = calc_angle(points[i], points[i+1])
                if sq_cap:
                    point = point[0] - weight/2 * cos(aaa), point[1] - weight/2 * sin(aaa)
                wrap_line(point, point, points[i+1], points[i+2], weight)
    
            elif i == len(points)-2:
                next_p = points[i+1]
                aaa = calc_angle(points[i], next_p)
                if sq_cap:
                    next_p = next_p[0] + weight/2 * cos(aaa), next_p[1] + weight/2 * sin(aaa)
                wrap_line(points[i-1], points[i], next_p, next_p, weight)
            else:
                # print ( calc_delta(points[i-1], points[i], points[i+1]) )
                wrap_line(points[i-1], points[i], points[i+1], points[i+2], weight)
    

    0_1527376252599_amp.jpg



  • and thanks to @gferreira who helped me getting the initial bits!



  • Thanks for sharing this!—I'll have a look. I agree that Crouwel used that beveled join pretty consistently. It's just that the underlying logic of the letterforms—the grid of nodes, the segments used to make up the glyph paths—is independent of how the strokes are styled. (Thus he had for example a version of the alphabet with dots in the dimensions of his columns and rows.)



  • @jo good times!



  • @jo Johannes, thanks again for posting your code. I'm off to a promising start—the code is handling the 90° bends perfectly and capping the ends correctly; these paths all have 3 or more points to them. But it's not handling the 2-point segments correctly, as you can see here (the red circles mark the node locations in the grid). I'm digging into the code now, but in case you have any ideas … ?

    0_1527973542779_Screen Shot 2018-06-02 at 16.58.26.png



  • I can confirm the sq_cap toggle does work and affects the polygon drawn around the bent paths, but not the two-segment paths.



  • Figured it out, seems obvious: the <if len(points) == 2:> option wasn't adjusting for the square cap. Just took some adapting the code and it all works great, now. Thanks again!



  • @mauricemeilleur sorry. I made the changes and updated the post. Only after implementing the changes in my local version did I realize that i forgot to adapt the len(points) == 2 option. but great you figured it out anyway,
    good luck!



  • The last thing will be to provide for the case where the points are in a continuous chain, like the <o>. I'm pretty sure I know what to do, and it shouldn't be that hard, but I have some other problems with the Crouwel code to fix first (all to do with graphing and finding valid paths—this whole project is forcing me to teach myself the basics of computer science as I go). I'll post the revised and extended code here when it's done.