Jak utworzyć kreskę pióra o szerokiej krawędzi przez przesuwanie stałego kształtu wzdłuż ścieżki
Aby utworzyć kreskę pióra o szerokiej krawędzi, zdefiniuj wąską prostokątną stalówkę, utrzymuj jej kąt na stałym poziomie i przesuwaj ją wzdłuż ścieżki. Kreska jest sumą wszystkich pozycji zajmowanych przez ten prostokąt. Jej grubość zmienia się w zależności od kierunku ruchu, mimo że sama stalówka nigdy nie zmienia rozmiaru.
W tym przewodniku pokazano, jak skonstruować taką geometrię na potrzeby rysunku wektorowego lub akcentu postaci. Zdefiniujesz stalówkę, przesuniesz ją wzdłuż łamanej w sposób ciągły i wygenerujesz SVG za pomocą kompletnego programu w Pythonie. Główna konstrukcja pozwala uniknąć przerw, które mogą powstawać, gdy pędzel jest symulowany za pomocą oddzielnych odcisków.
Co oznacza przesuwanie stałego kształtu?
Podziel rysunek na dwa elementy wejściowe: ścieżkę opisującą ruch punktu odniesienia stalówki oraz obrys opisujący obszar zajmowany przez stalówkę w jednej pozycji. Umieść punkt odniesienia obrysu w początku układu współrzędnych. W każdej pozycji na ścieżce dodaj tę pozycję do każdego punktu obrysu.
Matematycznie otrzymany zbiór to S = {p + n | p belongs to P, n belongs to N}, gdzie P jest ścieżką, a N — stalówką. Jest to suma Minkowskiego, zgodna z definicją punktową podaną w podręczniku użytkownika CGAL dotyczącym sum Minkowskiego 2D.
W tej konstrukcji „stały” oznacza, że zarówno obrys, jak i jego orientacja pozostają niezmienne we współrzędnych rysunku. Stalówka przesuwa się bez obracania, by podążać za ścieżką. Umieszczenie punktu odniesienia w środku prostokąta sprawia, że ścieżka opisuje środek poruszającej się stalówki.
Jak kąt stalówki i kierunek ruchu określają grubość?
Wybierz trzy parametry: długi wymiar prostokąta w, jego krótki wymiar h oraz kąt α jego dłuższej krawędzi. Niewielka dodatnia wartość h zapewnia stalówce widoczną minimalną grubość. Idealna stalówka w kształcie odcinka ma zerową grubość i może pozostawić ślad o zerowym polu, jeśli przesunie się ją dokładnie wzdłuż jej własnego kierunku.
Dla prostego odcinka ścieżki o kącie kierunku θ szerokość prostokątnej stalówki mierzona prostopadle do kierunku ruchu wynosi:
width across the stroke = w × |sin(θ − α)| + h × |cos(θ − α)|
Wzór ten wynika z rzutowania dwóch prostopadłych wymiarów prostokąta na normalną do ścieżki. Opisuje on prosty odcinek lub lokalny rzut obrysu wzdłuż gładkiej krzywej; nakładające się części zakrzywionej kreski mogą sprawić, że jej cała sylwetka będzie szersza.
Dla przykładowej stalówki o wymiarach 24 na 3 jednostki ruch równoległy do dłuższej krawędzi tworzy pas o szerokości 3 jednostek. Ruch prostopadły tworzy pas o szerokości 24 jednostek. Przy różnicy kąta wynoszącej 45° szerokość to w przybliżeniu (24 + 3) / √2, czyli 19,09 jednostki. Są to wymiary obliczone, a nie wyniki pomiaru pędzla.
Przed rysowaniem skomplikowanej krzywej użyj tych trzech kierunków jako testu kalibracyjnego. Pozwalają sprawdzić, czy kąt i wymiary działają zgodnie z założeniami, bez konieczności analizowania kilku nakładających się zakrętów.
Jak przesuwać stalówkę w sposób ciągły, unikając przerw między odciskami?
Dla wypukłej stalówki poruszającej się wzdłuż jednego prostego odcinka utwórz jej kopię na każdym końcu odcinka, a następnie oblicz otoczkę wypukłą wszystkich wierzchołków obu kopii. Ta otoczka dokładnie odpowiada obszarowi omiatanemu podczas przesuwania stalówki wzdłuż odcinka.
Powód jest prosty: każde pośrednie położenie jest interpolacją liniową między położeniami na końcach. Ponieważ stalówka jest wypukła, otoczka zawiera dokładnie te interpolowane położenia. Ta konstrukcja dla odcinka wynika bezpośrednio z definicji sumy Minkowskiego, a nie z osobnej funkcji pędzla.
W przypadku prostokąta każdy odcinek daje tylko osiem punktów kandydujących: cztery narożniki na początku i cztery na końcu. Oblicz ich otoczkę, powtórz dla każdej kolejnej pary punktów ścieżki i połącz uzyskane wielokąty. Sąsiednie omiatane obszary mają wspólne pełne położenie stalówki w punkcie końcowym, więc geometria zapewnia ciągłe pokrycie.
Nie obliczaj jednej otoczki wypukłej dla całej ścieżki. Wypełniłoby to obszary przy zakrętach i usunęło zamierzone wcięcia. Otoczka dotyczy każdego odcinka osobno; cała kreska jest ich sumą.
Wklęsła stalówka wymaga innego podejścia. Utworzenie jej otoczki wypełniłoby wcięcia. Podziel ją na wypukłe części, omiataj każdą z nich i połącz wyniki. CGAL opisuje pokrewne podejście polegające na dekompozycji i sumie dla sum Minkowskiego wielokątów niewypukłych.
Jak zaimplementować przesuwanie w Pythonie?
Poniższy kompletny program wymaga Pythona 3 i jego biblioteki standardowej. Definiuje przykładową ścieżkę złożoną z pięciu punktów, tworzy prostokątną stalówkę i wypisuje dokument SVG zawierający cztery wypełnione wielokąty omiatania. Zapisz go jako broad_edge.py, a następnie uruchom python broad_edge.py > stroke.svg, aby utworzyć rysunek.
Współrzędne są zgodne z konwencją SVG używaną w tym przykładzie: x rośnie w prawo, a y w dół. W związku z tym dodatni kąt w tym programie przechyla stalówkę zgodnie z ruchem wskazówek zegara na ekranie. W innych interfejsach rysunkowych mogą obowiązywać odmienne konwencje kątów.
from math import cos, sin, radians
def hull(points):
pts = sorted(set(points))
if len(pts) <= 1:
return pts
def cross(o, a, b):
return ((a[0] - o[0]) * (b[1] - o[1])
- (a[1] - o[1]) * (b[0] - o[0]))
def half(sequence):
out = []
for p in sequence:
while len(out) >= 2 and cross(out[-2], out[-1], p) <= 0:
out.pop()
out.append(p)
return out
return half(pts)[:-1] + half(reversed(pts))[:-1]
def sweep(path, width=24, thickness=3, angle=30):
if not path or width <= 0 or thickness <= 0:
raise ValueError(
'Provide a nonempty path and positive nib dimensions'
)
a = radians(angle)
u = (cos(a), sin(a))
v = (-sin(a), cos(a))
nib = [
(s * width * u[0] / 2 + t * thickness * v[0] / 2,
s * width * u[1] / 2 + t * thickness * v[1] / 2)
for s, t in [(-1, -1), (1, -1), (1, 1), (-1, 1)]
]
pairs = list(zip(path, path[1:])) or [(path[0], path[0])]
return [
hull([(p[0] + q[0], p[1] + q[1])
for p in (start, end) for q in nib])
for start, end in pairs
]
path = [(40, 45), (85, 125), (140, 150), (195, 110), (245, 45)]
polygons = sweep(path)
all_points = [p for polygon in polygons for p in polygon]
x0, y0 = (min(p[i] for p in all_points) - 5 for i in (0, 1))
x1, y1 = (max(p[i] for p in all_points) + 5 for i in (0, 1))
svg = [
'<svg xmlns="http://www.w3.org/2000/svg" '
f'viewBox="{x0} {y0} {x1 - x0} {y1 - y0}">',
'<g fill="#23364d" stroke="none">'
]
for polygon in polygons:
coordinates = ' '.join(f'{x:.6f},{y:.6f}' for x, y in polygon)
svg.append(f'<polygon points="{coordinates}"/>')
svg.extend(['</g>', '</svg>'])
print('\n'.join(svg))
W przypadku pojedynczego punktu ścieżki program rysuje obrys jednej stalówki. Powtarzające się kolejne punkty również dają obrys, bez konieczności obliczania kierunku. Puste ścieżki i niedodatnie wymiary stalówki powodują błąd. Podaj pary skończonych wartości liczbowych; ten zwięzły przykład nie weryfikuje zaimportowanych danych ścieżki.
Konstrukcję uruchomiono dla przykładowych współrzędnych: powstały cztery wielokąty omiatania o sześciu wierzchołkach każdy, a wygenerowany SVG został poprawnie sparsowany jako XML. Sprawdzono również przypadki pojedynczego i powtarzającego się punktu. Testy te weryfikują konstrukcję i strukturę dokumentu; nie są testem renderowania wizualnego.
Jak obsługiwać krzywe, narożniki i zakończenia?
Przykład jest dokładny dla swojej łamanej, z uwzględnieniem arytmetyki zmiennoprzecinkowej i zaokrąglania współrzędnych. Aby użyć krzywej Béziera, najpierw przybliż ją odcinkami prostymi, a następnie zastosuj to samo przesuwanie. Dodawaj więcej odcinków tam, gdzie krzywa mocno się zakrzywia, zamiast rozmieszczać wszystkie próbki równomiernie względem parametru krzywej.
Dobierz tolerancję przybliżenia do planowanego rozmiaru wyjściowego. Na przykład odchylenie o ćwierć piksela może być początkowym celem dla obrazu ekranowego, po którym należy przeprowadzić kontrolę w największej planowanej skali. To przykładowy wybór inżynierski, a nie uniwersalny próg jakości. Co ważne, dodawanie podziałów wzdłuż już prostego odcinka nie zmienia omiatanego obszaru.
Oddzielne ścieżki, po których pióro było uniesione, pozostaw rozdzielone. Przekazanie punktów dwóch niepołączonych kresek w jednym wywołaniu utworzyłoby niepożądany łączący obszar omiatania. Generuj każdą kreskę osobno i dodawaj jej wielokąty do wyniku.
Na końcu ścieżki stalówka zachowuje cały swój obrys. Na narożniku sąsiednie obszary omiatania nakładają się. Model ten nie dodaje dodatkowego łączenia typu miter ani zaokrąglonego zakończenia. Standardowe właściwości kresek SVG definiują własne sposoby tworzenia zakończeń i łączeń, więc ich dodanie wprowadziłoby geometrię wykraczającą poza przesuwanie stałej stalówki. Zobacz definicje zakończeń i łączeń kresek w SVG 2.
Kiedy trzeba połączyć wielokąty?
Program eksportuje nakładające się wypełnione wielokąty. W podglądzie jednolitego koloru przedstawiają one pokrycie uzyskane przez omiatanie i nie wymagają biblioteki do sumowania wielokątów. Pozostają jednak oddzielnymi edytowalnymi obiektami; program nie oblicza jednej scalonej granicy.
Jeśli potrzebujesz czystej sylwetki do dalszej edycji konturu, oblicz sumę boolowską wszystkich wielokątów odcinków. Zachowaj ewentualne otwory w wyniku. Zapętlona ścieżka może otaczać niezamalowany obszar, a wypełnienie każdego zamkniętego konturu zmieniłoby kreskę.
Przezroczystość także wymaga przemyślanego podejścia. Osobne zastosowanie częściowego fill-opacity do nakładających się wielokątów może przyciemnić obszary ich przecięcia. Aby uzyskać jednolitą półprzezroczystość, zastosuj krycie do grupy zawierającej wielokąty albo scal geometrię przed ustawieniem krycia wypełnienia. SVG rozróżnia krycie wypełnienia obiektu i krycie grupy w swojej specyfikacji malowania.
| Oczekiwany wynik | Odpowiedni następny krok |
|---|---|
| Podgląd w jednolitym kolorze | Pozostaw wypełnione wielokąty omiatania |
| Jednolity półprzezroczysty podgląd | Zastosuj krycie do grupy zawierającej wielokąty |
| Jedna edytowalna sylwetka | Oblicz sumę boolowską wielokątów omiatania |
| Niestandardowa wklęsła stalówka | Podziel stalówkę przed przesuwaniem |
| Gładka zakrzywiona kreska | Przybliż krzywą, a następnie omiataj jej odcinki |
Jak zdiagnozować nieoczekiwaną kreskę?
Zacznij od kierunków kalibracyjnych, a następnie sprawdź ścieżkę i konstrukcję wyniku. Zmieniaj tylko jedno wejście naraz, aby przyczyna widocznej różnicy była jasna.
- Grubość pozostaje niemal stała: Sprawdź, czy stalówka obraca się wraz ze ścieżką. Jej kąt musi pozostać stały we współrzędnych rysunku.
- Między śladami widać przerwy: Zastąp oddzielne odciski konstrukcją otoczki dla każdego odcinka.
- Szeroki klin wypełnia wnętrze zakrętu: Sprawdź, czy przypadkiem nie utworzyłeś otoczki dla kilku odcinków ścieżki naraz.
- Końce wyglądają na zaokrąglone: Sprawdź, czy nie dodano zwykłej kreski albo innego obrysu stalówki.
- Gładka krzywa źródłowa wygląda kanciasto: Doprecyzuj jej przybliżenie łamaną w okolicach zakrętów.
- Pojawiają się nieoczekiwane łączniki: Przekazuj niepołączone ścieżki w oddzielnych wywołaniach przesuwania.
Jeśli porównujesz wynik z narzędziem Kaligrafia w Inkscape, jego dokumentacja zawiera przydatne wskazówki dotyczące parametrów: wartość Fixation 100 utrzymuje stały kąt, Thinning 0 usuwa zmienność szerokości zależną od prędkości, a ustawienia nacisku i pochylenia mogą zmieniać szerokość i kąt. Wyłącz te zmienne parametry, analizując zachowanie stałej stalówki. Ustawienia te opisują opcje narzędzia; nie dowodzą, że jego wynik jest identyczny z tą konstrukcją prostokątną. Zobacz dokumentację paska narzędzi Kaligrafia w Inkscape.
Niezawodna kreska o szerokiej krawędzi zaczyna się od prostej ścieżki kalibracyjnej i jasno zdefiniowanego obrysu. Gdy testy dla ruchu równoległego, prostopadłego i ukośnego dadzą poprawne wyniki, zastosuj to samo przesuwanie wzdłuż wybranej ścieżki, doprecyzuj krzywe odcinki stosownie do skali wyjściowej i połącz wielokąty, jeśli kolejny etap edycji wymaga jednej sylwetki.
