Last active
March 23, 2019 17:46
-
-
Save stenson/0c10639a8f91193a08b8fa2fff868bf4 to your computer and use it in GitHub Desktop.
some code for almost very accurately setting text on a path
This file contains 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
# 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