Následuj cestu
Nástroje, se kterými jsme dosud pracovali, umožnily vytvořit cestu pro nějaký systém (wxPython, SVG), a tu cestu jsme potom mohli vykreslit jako celek. Dnes bych ale chtěl získat matematickou reprezentaci cesty a s tou pak dál pracovat.
Bézierovy křivky
Pokud už nějaký čas programujete, pojem Bézierova křivka už jste určitě zaslechli. Nebudeme ji zde vysvětlovat, bude stačit zjednodušeně říct, že pro fonty potřebujeme kvadriky a kubiky (v rovině). Kvadrika je tvořena třemi body, kubika čtyřmi. Striktně vzato, potřebujeme ještě rovnou čáru, ta je tvořena dvěma body.
pip install bezier
Začneme známou game loop z pygame
, jen z ní vyhodíme všechno, co souvisí
s fontem, a přidáme vytvoření a vykreslení několika křivek:
import bezier
def make_curves():
ret = []
ret.append(bezier.Curve.from_nodes([
[100, 100],
[100, 110],
]))
ret.append(bezier.Curve.from_nodes([
[100, 100, 200],
[110, 300, 300],
]))
ret.append(bezier.Curve.from_nodes([
[200, 300],
[300, 300],
]))
ret.append(bezier.Curve.from_nodes([
[300, 300, 100, 100],
[300, 100, 200, 100],
]))
return ret
def draw_curves(screen, curves):
for c in curves:
last_pt = None
for i in range(21):
v = c.evaluate(i / 20)
v = (v[0][0], v[1][0])
if not last_pt is None:
pygame.draw.line(screen, (255, 0, 255), last_pt, v)
last_pt = v
Jak vidíte, vytvořili jsme pole křivek, které na sebe navazují.
Animovat bod po každé křivce zvlášť je jednoduché. Rozdělme si
animaci na 100 kroků. Pak pro křivku můžeme použít funkci
bezier.Curve.evaluate(t)
, kde t
je hodnota od 0 do 1,
tedy náš krok vydělíme stovkou. Asi takto:
def animate_point_each_curve(screen, curves, idx):
for c in curves:
v = c.evaluate(idx / 100)
v = (v[0][0], v[1][0])
pygame.draw.circle(screen, (0, 0, 255), v, 3)
Všimněte si té pomalé tečky úplně nahoře. Dává to perfektní smysl. Je to tak nejkratší křivka (úplně první úsečka), a těch 100 kroků se na ní provede za stejnou dobu, jako na té nejdelší. To budeme chtít vylepšit.
Animace po celé cestě
Nejprve ale budeme animovat bod po celé složené křivce. Je to zase jednoduché, prostě si těch 100 kroků rozdělíme na jednotlivé úseky. Primitivní řešení, které trpí stejným neduhem, jako předchozí animace:
def animate_point_as_single_curve(screen, curves, idx):
whole_len = 100 * len(curves)
n_idx = whole_len * idx // 100
curve_idx = n_idx // 100
c = curves[curve_idx]
n_idx = n_idx % 100
if n_idx > 100:
n_idx = 100
v = c.evaluate(n_idx / 100)
v = (v[0][0], v[1][0])
pygame.draw.circle(screen, (0, 255, 0), v, 6)
K vylepšení nám pomůže funkce bezier.Curve.length
. Naši skupinu křivek
rozdělíme proporcionálně podle délek, a tím se nám animace stane plynulou.
def animate_smooth(screen, curves, idx):
len_all = 0
for c in curves:
len_all += c.length
projected_idx = len_all * idx / 100
previous_len = 0
current_len = 0
for c in curves:
current_len += c.length
if projected_idx < current_len:
partial = projected_idx - previous_len
v = c.evaluate(partial / c.length)
v = (v[0][0], v[1][0])
pygame.draw.circle(screen, (255, 0, 0), v, 6)
break
previous_len = current_len
Sami vidíte, že ta třetí animace už je plynulá. Celý kód najdete zde.
Píšeme po cestě
Teď už nám nic nebrání v tom, abychom na cestě vysázeli nápis. Nebo ano?
def draw_text(screen, curves, text):
current_pos = 0
curve_start = 0
text_ptr = -1
curve_ptr = 0
while True:
c = curves[curve_ptr]
if curve_start + c.length < current_pos:
curve_ptr += 1
curve_start += c.length
if curve_ptr >= len(curves):
break
continue
frac = (current_pos - curve_start) / c.length
v = c.evaluate(frac)
v = (v[0][0], v[1][0])
text_ptr += 1
if text_ptr >= len(text):
break
img1 = font.render(text[text_ptr], True, (255, 255, 255))
screen.blit(img1, v)
current_pos += 15
Tohle samozřejmě není úplně to, co jsme chtěli. Budeme muset jednotlivé znaky
otočit. Ale jak? Na pomoc nám přijde funkce bezier.Curve.evaluate_hodograph
.
Ta ukáže směr, kterým v tom bodě křivka míří. A to je přesně to, co potřebujeme.
# ...
img1 = font.render(text[text_ptr], True, (255, 255, 255))
hodograph = c.evaluate_hodograph(frac)
angle = math.atan2(hodograph[1][0], hodograph[0][0])
img1 = pygame.transform.rotate(img1, math.degrees(angle))
screen.blit(img1, v)
# ...
To už je lepší, ale ještě to potřebujeme otočit podle normály, což je jak víte vektor kolmý na směrový vektor.
# ...
img1 = font.render(text[text_ptr], True, (255, 255, 255))
hodograph = c.evaluate_hodograph(frac)
angle = math.atan2(-hodograph[1][0], hodograph[0][0])
img1 = pygame.transform.rotate(img1, math.degrees(angle))
screen.blit(img1, v)
# ...
Poslední chyba spočívá v tom, že písmena vykreslujeme od levého horního
rohu. Náš finální Surface
vycentrujeme. Zároveň se při vykreslování
nebudeme posouvat o 15, ale o šířku vykresleného písmena.
# ...
img1 = font.render(text[text_ptr], True, (255, 255, 255))
img_width = img1.get_width()
hodograph = c.evaluate_hodograph(frac)
angle = math.atan2(-hodograph[1][0], hodograph[0][0])
img1 = pygame.transform.rotate(img1, math.degrees(angle))
screen.blit(img1, (v[0] - img2.get_width() / 2, v[1] - img2.get_height() / 2))
current_pos += img_width
# ...
Dokonalé to není, ostrá zatáčka dělá velký problém, ale i neostrá zatáčka způsobí, že se písmena nahoře nebo dole přiblíží k sobě.
Zpět k fontu
Abych pravdu řekl, moc se mi nechce zacházet do detailů. Budete se muset spokojit s jednou malou ukázkou a kódem.
Komentáře byly zrušeny
V EU teď máme složitou situaci s Cookies. Na komentáře jsem používal jistou službu třetí strany. Ta však používá Cookies poměrně, ehm, benevolentně. Tak jsem se rozhodl komentáře zrušit. Pokud chcete, můžete mi napsat přímo