Následuj cestu

... Petr Blahoš, 22. 12. 2023 Font Python

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)

Animace na každé křivce zvlášť

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

Porovnání animací

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

Text na křivce bez rotace

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)
        # ...

Text na křivce s rotací podle směrového vektoru

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)
        # ...

Text na křivce s rotací podle normály

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
        # ...

Text na křivce

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.

Text na textu


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