Last active
April 13, 2024 10:25
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# [email protected] | |
# 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