Skip to content

Instantly share code, notes, and snippets.

@LettError
Last active June 29, 2025 08:16

Revisions

  1. LettError revised this gist Jun 29, 2025. 1 changed file with 31 additions and 14 deletions.
    45 changes: 31 additions & 14 deletions guide_at_slantangle_tangent.py
    Original file line number Diff line number Diff line change
    @@ -1,11 +1,14 @@
    # coding: utf-8
    # menuTitle : Guide at Slant Angle Tangent
    # shortCut : command+shift+f


    # [email protected]
    # April 2024
    # version 1
    # June 2025
    # version 1.1

    # For robofont
    # Dedicated to my github sponsors who encourate explorations like this.
    # Dedicated to my github sponsors who encourate explorations like this.z

    # this can find the t for horizontals in a cubic bezier segment.
    # this can find the tangent at any angle (by rotating the segment and finding horizontals)
    @@ -64,6 +67,7 @@ def add(p1, p2):

    def findHorizontals(glyph):
    results = []
    res = 0.01
    for contourIndex, c in enumerate(glyph.contours):
    bps = c.bPoints
    l = len(bps)
    @@ -82,6 +86,11 @@ def findHorizontals(glyph):
    elif 0 < value < 1:
    cpt = cubicPointAtT(s[0], s[1], s[2], s[3], value)
    results.append((contourIndex, bPointIndex, value))
    elif -res < value < 1+res:
    # this catches values that are very very close
    # to 0 or 1 but on the outer edge
    cpt = cubicPointAtT(s[0], s[1], s[2], s[3], value)
    results.append((contourIndex, bPointIndex, value))
    else:
    pass
    return results
    @@ -102,7 +111,7 @@ def findTangentInGlyph(glyph, angle):
    points = []
    results = findHorizontals(g2)
    for contourIndex, bPointIndex, value in results:
    pt = findPointFromT(g, contourIndex, bPointIndex, value)
    pt = findPointFromT(glyph, contourIndex, bPointIndex, value)
    points.append(pt)
    return points

    @@ -117,11 +126,18 @@ def nearSelected(glyph, pt, dst=100):
    if len(near) > 0:
    return True
    return False

    def recenterGuide(pt, angle, d=300):
    x, y = pt
    dx = math.cos(radians(angle)) * d
    dy = math.sin(radians(angle)) * d
    return x+dx, y+dy


    # - - -
    g = CurrentGlyph()
    glyph = g.getLayer("foreground")
    glyph = CurrentGlyph()

    #glyph = glyph.getLayer("foreground")

    # this cleans guides with names that start with "angled_"
    guideNamePrefix = "angled_"
    @@ -135,19 +151,20 @@ def nearSelected(glyph, pt, dst=100):

    # this can be any other angle of course
    # in case someone wants to write a UI for it.
    angle = g.font.info.italicAngle
    fontItalicAngle = glyph.font.info.italicAngle
    if fontItalicAngle is None:
    fontItalicAngle = 0

    # add guides for candidate tangents near selected points in the glyph
    # or all candidates if there is no selection
    for p in findTangentInGlyph(g, angle):
    for p in findTangentInGlyph(glyph, fontItalicAngle):
    if nearSelected(glyph, p):
    glyph.appendGuideline(p, angle, color=(1,.5,0,1), name=f"{guideNamePrefix}{angle:3.3f}")
    glyph.appendGuideline(p, angle-90, color=(1,.25,0,1), name=f"{guideNamePrefix}_ortho_{angle:3.3f}")
    glyph.appendGuideline(recenterGuide(p, fontItalicAngle), fontItalicAngle, color=(1,.5,0,1), name=f"{guideNamePrefix}{fontItalicAngle:3.3f}")
    glyph.appendGuideline(recenterGuide(p, fontItalicAngle-90), fontItalicAngle-90, color=(1,.25,0,1), name=f"{guideNamePrefix}_ortho_{fontItalicAngle:3.3f}")

    # this adds guides on tangents +90 from the given angle.
    # maybe not what you need. Easy to remove.
    for p in findTangentInGlyph(g, angle + 90):
    for p in findTangentInGlyph(glyph, fontItalicAngle + 90):
    if nearSelected(glyph, p):
    glyph.appendGuideline(p, angle, color=(0,.5,1,1), name=f"angled_{angle:3.3f}")
    glyph.appendGuideline(p, angle-90, color=(1,.25,1,1), name=f"angled_ortho_{angle:3.3f}")

    glyph.appendGuideline(p, fontItalicAngle, color=(0,.5,1,1), name=f"angled_{fontItalicAngle:3.3f}")
    glyph.appendGuideline(p, fontItalicAngle-90, color=(1,.25,1,1), name=f"angled_ortho_{fontItalicAngle:3.3f}")
  2. LettError revised this gist Apr 13, 2024. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion guide_at_slantangle_tangent.py
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,8 @@
    # April 2024
    # version 1

    # for robofont
    # For robofont
    # Dedicated to my github sponsors who encourate explorations like this.

    # this can find the t for horizontals in a cubic bezier segment.
    # this can find the tangent at any angle (by rotating the segment and finding horizontals)
  3. LettError created this gist Apr 13, 2024.
    152 changes: 152 additions & 0 deletions guide_at_slantangle_tangent.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,152 @@


    # [email protected]
    # April 2024
    # version 1

    # for robofont

    # this can find the t for horizontals in a cubic bezier segment.
    # this can find the tangent at any angle (by rotating the segment and finding horizontals)
    # this adds guides to the current glyph at the font's italic angle
    # .. near selected points. To prevent a mass of guidelines to be added.

    # to do: find origins for the guides that will give the same line
    # but don't have the guide label so near the tangent point.

    # some of the reasoning:
    # the function of the tangent is the derivative of the cubic.
    # https://math.stackexchange.com/questions/477165/find-angle-at-point-on-bezier-curve
    # 𝐏′(𝑡)=(1−𝑡)^2(𝐏1−𝐏0)+2𝑡(1−𝑡)(𝐏2−𝐏1)+𝑡^2(𝐏3−𝐏2)

    # code should be similar for px = 0
    # solve t for py = 0
    # this is my solution, it seems to work.
    #py = (1-t)**2 * a + 2*t*(1-t) * b + t**2 * c
    #py = (1-t)(1-t) * a + 2*t*(1-t) * b + t**2 * c
    #py = (1 -2*t + t**2) * a + (2 * t -2 * t**2) * b + c*t**2
    #py = a - 2*a*t + a*t**2 + 2 * b * t - 2 * b * t**2 + c*t**2
    #py = a*t**2 -2*b*t**2 + c*t**2 - 2*a*t + 2 * b * t + a
    #py = (a - 2*b + c)*t**2 + (2*b - 2*a) * t + a

    import math
    from math import degrees, atan2, sin, cos, pi, radians, degrees
    from random import random
    from fontTools.misc.bezierTools import cubicPointAtT, solveQuadratic


    def tangentAtT(p0, p1, p2, p3, t):
    # not used in this script, but kept for reference
    # get the tangent point at t
    a = (1-t)**2
    b = 2*t*(1-t)
    c = t**2
    px = a * (p1[0]-p0[0]) + b*(p2[0]-p1[0]) + c*(p3[0]-p2[0])
    py = a * (p1[1]-p0[1]) + b*(p2[1]-p1[1]) + c*(p3[1]-p2[1])
    #return px + p0[0], py + p0[1]
    return px, py

    def t_for_horizontal(p0, p1, p2, p3, horizontal=True):
    if horizontal:
    i = 1
    else:
    i = 0
    a = (p1[i]-p0[i])
    b = (p2[i]-p1[i])
    c = (p3[i]-p2[i])
    # solve the quadratic
    r = solveQuadratic((a - 2*b + c), (2*b - 2*a), a)
    return r

    def add(p1, p2):
    return p1[0]+p2[0],p1[1]+p2[1]

    def findHorizontals(glyph):
    results = []
    for contourIndex, c in enumerate(glyph.contours):
    bps = c.bPoints
    l = len(bps)
    for bPointIndex, bp1 in enumerate(bps):
    bp2 = bps[(bPointIndex+1)%l]
    if bp1.type != "curve" and bp2.type != "curve": continue
    # so the segment we're interested in will bp1.anchor, bp1.out, bp2.in, bp2.anchhor
    s = (bp1.anchor, add(bp1.anchor,bp1.bcpOut), add(bp2.anchor,bp2.bcpIn), bp2.anchor)
    r = t_for_horizontal(s[0], s[1], s[2], s[3], 1)
    for value in r:
    # this does not catch everything --
    if value == 0:
    results.append((contourIndex, bPointIndex, 0))
    elif value == 1:
    results.append((contourIndex, bPointIndex, 1 ))
    elif 0 < value < 1:
    cpt = cubicPointAtT(s[0], s[1], s[2], s[3], value)
    results.append((contourIndex, bPointIndex, value))
    else:
    pass
    return results

    def findPointFromT(glyph, contourIndex, bPointIndex, value):
    bps = glyph.contours[contourIndex].bPoints
    bp1 = bps[bPointIndex]
    bp2 = bps[(bPointIndex + 1)%len( bps)]
    s = (bp1.anchor, add(bp1.anchor,bp1.bcpOut), add(bp2.anchor,bp2.bcpIn), bp2.anchor)
    cpt = cubicPointAtT(s[0], s[1], s[2], s[3], value)
    return cpt

    def findTangentInGlyph(glyph, angle):
    # pay attention, from robofont slant angle
    angle = -(angle - 90)
    g2 = glyph.copy()
    g2.rotate(angle)
    points = []
    results = findHorizontals(g2)
    for contourIndex, bPointIndex, value in results:
    pt = findPointFromT(g, contourIndex, bPointIndex, value)
    points.append(pt)
    return points

    def nearSelected(glyph, pt, dst=100):
    # if the point is close to a currently selected point
    near = []
    if len(glyph.selectedPoints) == 0:
    return True
    for p in glyph.selectedPoints:
    if math.hypot(p.x-pt[0],p.y-pt[1]) <= dst:
    near.append(pt)
    if len(near) > 0:
    return True
    return False


    # - - -
    g = CurrentGlyph()
    glyph = g.getLayer("foreground")

    # this cleans guides with names that start with "angled_"
    guideNamePrefix = "angled_"
    remove = []
    for guide in glyph.guidelines:
    if guide.name is not None:
    if guideNamePrefix in guide.name:
    remove.append(guide)
    for guide in remove:
    glyph.removeGuideline(guide)

    # this can be any other angle of course
    # in case someone wants to write a UI for it.
    angle = g.font.info.italicAngle

    # add guides for candidate tangents near selected points in the glyph
    # or all candidates if there is no selection
    for p in findTangentInGlyph(g, angle):
    if nearSelected(glyph, p):
    glyph.appendGuideline(p, angle, color=(1,.5,0,1), name=f"{guideNamePrefix}{angle:3.3f}")
    glyph.appendGuideline(p, angle-90, color=(1,.25,0,1), name=f"{guideNamePrefix}_ortho_{angle:3.3f}")

    # this adds guides on tangents +90 from the given angle.
    # maybe not what you need. Easy to remove.
    for p in findTangentInGlyph(g, angle + 90):
    if nearSelected(glyph, p):
    glyph.appendGuideline(p, angle, color=(0,.5,1,1), name=f"angled_{angle:3.3f}")
    glyph.appendGuideline(p, angle-90, color=(1,.25,1,1), name=f"angled_ortho_{angle:3.3f}")