Skip to content

Instantly share code, notes, and snippets.

@LettError
Last active June 29, 2025 08:16
Show Gist options
  • Save LettError/38245266a54e4a19dffe870397046ad5 to your computer and use it in GitHub Desktop.
Save LettError/38245266a54e4a19dffe870397046ad5 to your computer and use it in GitHub Desktop.
For RoboFont. Add a guideline for points (near selected points in the glyph) on the curve where the tangent is at a given angle.
# coding: utf-8
# menuTitle : Guide at Slant Angle Tangent
# shortCut : command+shift+f
# [email protected]
# June 2025
# version 1.1
# For robofont
# 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)
# 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 = []
res = 0.01
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))
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
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(glyph, 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
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
# - - -
glyph = CurrentGlyph()
#glyph = glyph.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.
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(glyph, fontItalicAngle):
if nearSelected(glyph, p):
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(glyph, fontItalicAngle + 90):
if nearSelected(glyph, p):
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}")
@LettError
Copy link
Author

Update 1.1: catch more angles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment