Created
January 3, 2024 19:21
-
-
Save petrblahos/39ecbfc08f01d5c2838aa5b4ff0d0cfb to your computer and use it in GitHub Desktop.
Draw a text on a text outline.
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
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() |
Author
petrblahos
commented
Jan 3, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment