Skip to content

Instantly share code, notes, and snippets.

@stenson
Last active March 23, 2019 17:46
Show Gist options
  • Save stenson/0c10639a8f91193a08b8fa2fff868bf4 to your computer and use it in GitHub Desktop.
Save stenson/0c10639a8f91193a08b8fa2fff868bf4 to your computer and use it in GitHub Desktop.
some code for almost very accurately setting text on a path
# for path-segmenting
from fontTools.misc.bezierTools import calcCubicArcLength, splitCubicAtT
from fontTools.pens.recordingPen import RecordingPen
import math
# for text layout internals
from drawBot.context.baseContext import BaseContext
import CoreText
import AppKit
import Quartz
DEFAULT_INC = 0.0015
def calcRecordedPenLength(recording):
length = 0
for i, (t, pts) in enumerate(recording):
if t == "curveTo":
p1, p2, p3 = pts
p0 = recording[i-1][-1][-1]
length += calcCubicArcLength(p0, p1, p2, p3)
elif t == "lineTo":
pass # todo
return length
def subsegmentRecordedPen(recording, start=None, end=None, inc=DEFAULT_INC):
length = calcRecordedPenLength(recording)
ended = False
_length = 0
out = []
for i, (t, pts) in enumerate(recording):
if t == "curveTo":
p1, p2, p3 = pts
p0 = recording[i-1][-1][-1]
length_arc = calcCubicArcLength(p0, p1, p2, p3)
if _length + length_arc < end:
_length += length_arc
else:
t = inc
tries = 0
while not ended:
a, b = splitCubicAtT(p0, p1, p2, p3, t)
length_a = calcCubicArcLength(*a)
if _length + length_a > end:
ended = True
out.append(("curveTo", a[1:]))
else:
t += inc
tries += 1
if t == "lineTo":
pass # TODO
if not ended:
out.append((t, pts))
if out[-1][0] != "endPath":
out.append(("endPath",[]))
return out
def subsegmentPoint(bp, start=0, end=1, inc=DEFAULT_INC):
rp = RecordingPen()
try: bp.draw(rp);
except: bp.drawToPen(rp);
subsegment = subsegmentRecordedPen(rp.value, start=start, end=end, inc=inc)
try:
t, (a, b, c) = subsegment[-2]
tangent = math.degrees(math.atan2(c[1] - b[1], c[0] - b[0]) + math.pi*.5)
return c, tangent
except ValueError:
return None, None
def offset(x, y, ox, oy):
return (x + ox, y + oy)
def TransliterateCGPathToBezierPath(data, b):
bp = data["bp"]
o = data["offset"]
op = lambda i: offset(*b.points[i], *o)
if b.type == Quartz.kCGPathElementMoveToPoint:
bp.moveTo(op(0))
elif b.type == Quartz.kCGPathElementAddLineToPoint:
bp.lineTo(op(0))
elif b.type == Quartz.kCGPathElementAddCurveToPoint:
bp.curveTo(op(0), op(1), op(2))
elif b.type == Quartz.kCGPathElementAddQuadCurveToPoint:
bp.qCurveTo(op(0), op(1))
elif b.type == Quartz.kCGPathElementCloseSubpath:
bp.closePath()
else:
print(b.type)
def textOnAPath(bp, fs, inc=DEFAULT_INC):
obp = BezierPath() # where text will end up
context = BaseContext()
attributedString = context.attributedString(fs, None)
w, h = attributedString.size()
path, (x, y) = context._getPathForFrameSetter((0, 0, w+100, h+100)) # fudge
setter = CoreText.CTFramesetterCreateWithAttributedString(attributedString)
frame = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None)
ctLines = CoreText.CTFrameGetLines(frame)
origins = CoreText.CTFrameGetLineOrigins(frame, (0, len(ctLines)), None)
for i, (originX, originY) in enumerate(origins[0:1]): # can only display one 'line' of text
ctLine = ctLines[i]
ctRuns = CoreText.CTLineGetGlyphRuns(ctLine)
for ctRun in ctRuns:
attributes = CoreText.CTRunGetAttributes(ctRun)
font = attributes.get(AppKit.NSFontAttributeName)
baselineShift = attributes.get(AppKit.NSBaselineOffsetAttributeName, 0)
glyphCount = CoreText.CTRunGetGlyphCount(ctRun)
last_adv = (0, 0)
for i in range(glyphCount):
glyph = CoreText.CTRunGetGlyphs(ctRun, (i, 1), None)[0]
ax, ay = CoreText.CTRunGetPositions(ctRun, (i, 1), None)[0]
glyphPath = CoreText.CTFontCreatePathForGlyph(font, glyph, None)
p, tangent = subsegmentPoint(bp, start=last_adv[0], end=ax, inc=inc)
if glyphPath and p is not None:
gbp = BezierPath()
Quartz.CGPathApply(glyphPath, dict(offset=p, bp=gbp), TransliterateCGPathToBezierPath)
minx, miny, maxx, maxy = gbp.bounds()
gbp_w = maxx - minx
p2, tangent2 = subsegmentPoint(bp, start=p, end=ax+gbp_w/2, inc=inc)
if p == None or p2 == None:
break
gbp.rotate(tangent2-90, center=p)
gbp.drawToPen(obp)
last_adv = (ax, ay)
obp.optimizePath()
return obp
if __name__ == "__main__":
fill(1)
rect(0, 0, 1000, 1000)
translate(0, 0)
bp = BezierPath()
bp.oval(100, 100, 800, 800)
#bp.reverse() # do this is you want the text on the outside
bp.rotate(-45-90, center=(500, 500))
#fill(0, 0.1)
#drawPath(bp)
fs = FormattedString("hello world, this is some text on a path ...",
#font="NikolaiV0.2-BoldNarrow",
#tracking=2, # useful since the curve can spread/compress the text
fontSize=120)
tbp = textOnAPath(bp, fs)
fill(0)
drawPath(tbp)
#saveImage("~/Desktop/textonapath.png")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment