Skip to content

Instantly share code, notes, and snippets.

@petrblahos
Created January 3, 2024 19:21
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 petrblahos/39ecbfc08f01d5c2838aa5b4ff0d0cfb to your computer and use it in GitHub Desktop.
Save petrblahos/39ecbfc08f01d5c2838aa5b4ff0d0cfb to your computer and use it in GitHub Desktop.
Draw a text on a text outline.
from collections import defaultdict
import math
import sys
import time
import bezier
import pygame
import numpy as np
from fontTools.pens.basePen import (BasePen, )
from fontTools.ttLib import TTFont
class PrepPen(BasePen):
"""
Here we want to prepare the data for the animation. We basically take all
the points and store them in a list with the instuctions of that kind of
operation it is.
"""
def __init__(self, glyphSet, scale, flip_y):
BasePen.__init__(self, glyphSet)
self.last_pt = None
self.scale = scale
self.flip_y = flip_y
self.path = []
self.current_path = None
self.pts = []
def _moveTo(self, p):
p = (self.scale * p[0], self.flip_y - self.scale * p[1])
if self.current_path is None:
self.current_path = []
self.path.append(self.current_path)
self.pts.append(p)
def _lineTo(self, p):
p = (self.scale * p[0], self.flip_y - self.scale * p[1])
a = np.asfortranarray([
[self.pts[-1][0], p[0]],
[self.pts[-1][1], p[1]],
])
self.pts.append(p)
curve = bezier.Curve.from_nodes(a)
self.current_path.append(curve)
def _curveToOne(self, p1, p2, p3):
p1 = (self.scale * p1[0], self.flip_y - self.scale * p1[1])
p2 = (self.scale * p2[0], self.flip_y - self.scale * p2[1])
p3 = (self.scale * p3[0], self.flip_y - self.scale * p3[1])
a = np.asfortranarray([
[self.pts[-1][0], p1[0], p2[0], p3[0]],
[self.pts[-1][1], p1[1], p2[1], p3[1]],
])
self.pts.append(p3)
curve = bezier.Curve.from_nodes(a)
self.current_path.append(curve)
def _qCurveToOne(self, p1, p2):
p1 = (self.scale * p1[0], self.flip_y - self.scale * p1[1])
p2 = (self.scale * p2[0], self.flip_y - self.scale * p2[1])
a = np.asfortranarray([
[self.pts[-1][0], p1[0], p2[0], ],
[self.pts[-1][1], p1[1], p2[1], ],
])
self.pts.append(p2)
curve = bezier.Curve.from_nodes(a)
self.current_path.append(curve)
def _closePath(self):
a = np.asfortranarray([
[self.pts[-1][0], self.pts[0][0]],
[self.pts[-1][1], self.pts[0][1]],
])
curve = bezier.Curve.from_nodes(a)
self.current_path.append(curve)
self.pts = []
self.current_path = None
class SingleShape:
def __init__(self, character, curves, shift_x, font, font2):
self.character = character
self.font = font
self.font2 = font2
self.shift_x = shift_x
self.curves = curves
self.length = 0
self.letter_count = None
for c in curves:
self.length += c.length
def animate_smooth(self, surface, idx):
projected_idx = self.length * idx / 100
previous_len = 0
current_len = 0
for c in self.curves:
current_len += c.length
if projected_idx < current_len:
partial = projected_idx - previous_len
v = c.evaluate(partial / c.length)
v = (self.shift_x + v[0][0], v[1][0])
pygame.draw.circle(surface, (255, 0, 0), v, 10, 4)
break
previous_len = current_len
def draw_text(self, surface, text, idx):
current_pos = 0
curve_start = 0
text_ptr = 0
curve_ptr = 0
while True:
c = self.curves[curve_ptr]
if curve_start + c.length < current_pos:
curve_ptr += 1
curve_start += c.length
if curve_ptr >= len(self.curves):
break
continue
frac = (current_pos - curve_start) / c.length
v = c.evaluate(frac)
v = (v[0][0] + self.shift_x, v[1][0])
text_ptr += 1
img1 = self.font.render(text[text_ptr % len(text)], True, (255, 255, 255))
hodograph = c.evaluate_hodograph(frac)
angle = math.atan2(-hodograph[1][0], hodograph[0][0])
img2 = pygame.transform.rotate(img1, math.degrees(angle))
surface.blit(
img2, (v[0] - img2.get_width() / 2, v[1] - img2.get_height() / 2))
current_pos += img1.get_width()
if not self.letter_count:
self.letter_count = text_ptr
def draw_text_wave(self, surface, text, idx):
current_pos = 0
curve_start = 0
text_ptr = 0
curve_ptr = 0
letter_count = self.letter_count or 1000
while True:
c = self.curves[curve_ptr]
if curve_start + c.length < current_pos:
curve_ptr += 1
curve_start += c.length
if curve_ptr >= len(self.curves):
break
continue
frac = (current_pos - curve_start) / c.length
v = c.evaluate(frac)
v = (v[0][0] + self.shift_x, v[1][0])
text_ptr += 1
img1 = self.font.render(text[text_ptr % len(text)], True, (255, 255, 255))
img_w = img1.get_width()
if text_ptr == idx % letter_count:
img1 = self.font2.render(text[text_ptr % len(text)], True, (255, 255, 255))
hodograph = c.evaluate_hodograph(frac)
angle = math.atan2(-hodograph[1][0], hodograph[0][0])
img2 = pygame.transform.rotate(img1, math.degrees(angle))
surface.blit(
img2, (v[0] - img2.get_width() / 2, v[1] - img2.get_height() / 2))
current_pos += img_w
if not self.letter_count:
self.letter_count = text_ptr
def draw_curves(self, surface):
for c in self.curves:
last_pt = None
for i in range(21):
v = c.evaluate(i / 20)
v = ((self.shift_x + v[0][0]), v[1][0])
if not last_pt is None:
pygame.draw.line(surface, (255, 0, 255), last_pt, v)
last_pt = v
class BezierLoop:
WIDTH = 1500
HEIGHT = 800
def __init__(self, font):
self.font = font
self.units_per_em = self.font['head'].unitsPerEm
self.ascent = self.font["hhea"].ascent
self.descent = self.font["hhea"].descent
self.cmap = self.font['cmap'].getBestCmap()
self.glyph_set = self.font.getGlyphSet()
self.kerning_pairs = self.prepare_kerning(self.font)
self.shapes = []
self.file_num = 0
if not "--save" in sys.argv:
self.save_screen = lambda: None
def make_curves(self, text):
ret = []
x0 = 150
pgm_font = pygame.font.SysFont("Arial", 20)
pgm_font2 = pygame.font.SysFont("Arial", 40)
last_glyph = None
for unicode in text:
glyph_name = self.cmap.get(ord(unicode))
if not glyph_name:
glyph_name = ".notdef"
glyph = self.glyph_set[glyph_name]
pen = PrepPen(self.glyph_set, scale=0.3, flip_y=600)
glyph.draw(pen)
x0 += self.kerning_pairs[(last_glyph, glyph_name)]
for p in pen.path:
self.shapes.append(SingleShape(unicode ,p, x0 * pen.scale, pgm_font, pgm_font2))
x0 += glyph.width
return ret
def prepare_kerning(self, font: TTFont) -> dict:
pairs = defaultdict(int)
if not "kern" in self.font:
return pairs
for kern_table in self.font['kern'].kernTables:
assert kern_table.version == 0, "kern table version other than 0 not supported"
pairs.update(kern_table.kernTable)
return pairs
def prepare_window(self):
self.screen = pygame.display.set_mode((self.WIDTH, self.HEIGHT))
self.screen.fill((0, 0, 0))
def run(self):
dt = 1
clock = pygame.time.Clock()
do_run = True
self.make_curves("Ahoj!")
cnt = -50
idx = -1
while do_run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
do_run = False
break
self.screen.fill((0, 0, 0))
for i in self.shapes:
# i.draw_curves(self.screen)
i.draw_text_wave(self.screen, "Ahoj!", idx)
i.animate_smooth(self.screen, idx % 100)
idx += 1
self.save_screen()
pygame.display.flip()
dt = clock.tick(60)
cnt += 1
if 0 == cnt % 500:
print("FPS: {}".format(1000 / dt))
if idx > 100:
break
def save_screen(self):
pygame.image.save(self.screen, "snaps/%04d.png" % self.file_num)
self.file_num += 1
if "__main__" == __name__:
f = TTFont("/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf")
pygame.init()
pgm = BezierLoop(f)
pgm.prepare_window()
pgm.run()
pygame.quit()
@petrblahos
Copy link
Author

text-on-a-font

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