Skip to content

Instantly share code, notes, and snippets.

@LettError
Last active April 13, 2024 10:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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.
# erik@letterror.com
# April 2024
# version 1
# 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)
# 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}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment