furious programming
2017-09-08 02:43

Ostatnio dość dużo dłubię w grafice – i przy jej tworzeniu, i przy programowaniu.

Jedną z takich dłubanin było malowanie tekstu z obramowaniem. Co prawda istnieje natywny dla systemu sposób takiego dekorowania tekstu (BeginPath i EndPath), jednak ma on pewne ograniczenia. Niewątpliwie jednym z najbardziej denerwujących jest brak wsparcia antialiasingu. Szkoda, bo dociąganie kobylastej biblioteki graficznej (lub kopiowanie z niej tego co interesujące) tylko po to, by namalować kilka napisów, nie brzmi zbyt rozsądnie.

Rozwiązanie znalazłem metodą prób i błędów, i choć nietypowe, może nieco dziwne, to zdaje egzamin. Chodzi o to, aby malować tekst bez tła. Przypisać kolor obramowania do Font.Color i namalować cztery razy docelowy tekst – odpowiednio o piksel wyżej i w lewo, wyżej i w prawo, niżej w lewo i niżej w prawo. Po tym wszystkim przywrócić ustawiony wcześniej kolor i namalować docelowy tekst w zadanej pozycji (już bez przesunięcia).


Załóżmy, że chcemy namalować na oliwkowym tle biały tekst z ciemniejszym obramowaniem. Najpierw wypełniamy obszar pod spodem standardową metodą, np. Canvas.FillRect:

Canvas.Brush.Color := $0034765E;
Canvas.FillRect(ClientRect);

Co da poniższy efekt:

0.png

Następnie ustawiamy właściwość Font:

Canvas.Font.Name := 'Ubuntu';
Canvas.Font.Size := 10;
Canvas.Font.Color := clWhite;
Canvas.Font.Style := [fsBold];

wpisujemy kolor obramowania do właściwości Pen (można go przekazać w parametrze – jak kto woli):

Canvas.Pen.Color := $00245241;

i na koniec wołamy metodę malującą tekst z obramowaniem:

Canvas.DrawTextOutlined(PosX, PosY, '4programmers.net');

Metoda działa w dwóch krokach – najpierw maluje cztery napisy z przesunięciem, tworząc ciemniejsze wypełnienie:

1.png

a na koniec maluje tekst w zadanym kolorze na wierzchu. Gotowy efekt poniżej:

2.png

Tekst w powiększeniu wygląda następująco:

3.png

Teraz kwestia kodu – w klasie TCanvas nie istnieje metoda DrawTextOutlined – stworzyłem dla niej helper. Cały kod niżej:

type
  TCanvasHelper = class helper for TCanvas
  public
    procedure DrawTextOutlined(AX, AY: Integer; const AText: String);
  end;
 
  procedure TCanvasHelper.DrawTextOutlined(AX, AY: Integer; const AText: String);
  var
    LOldBrushStyle: TBrushStyle;
    LOldFontColor: TColor;
  begin
    LOldBrushStyle := Self.Brush.Style;
    LOldFontColor := Self.Font.Color;
 
    Self.Brush.Style := bsClear;
    Self.Font.Color := Self.Pen.Color;
 
    Self.TextOut(AX - 1, AY - 1, AText);
    Self.TextOut(AX + 1, AY - 1, AText);
    Self.TextOut(AX - 1, AY + 1, AText);
    Self.TextOut(AX + 1, AY + 1, AText);
 
    Self.Font.Color := LOldFontColor;
    Self.TextOut(AX, AY, AText);
 
    Self.Brush.Style := LOldBrushStyle;
  end;

Zaletą jest to, że w ten sposób można namalować tekst zarówno o braku dodatkowych atrybutów, jak również tekst wytłuszczony, pochylony, podkreślony czy przekreślony. Drugą zaletą jest automatyczne dostosowanie się do systemowych ustawień antialiasingu – jeśli usługa rozmywania tekstu jest wyłączona to obramowanie będzie twarde, ale w dalszym ciągu widoczne.

Niestety, ale metoda ta ma też wady – nie nadaje się do malowania grubszego obramowania (przesunięcia większego niż 1px), co przy większych literach jest raczej konieczne. O ile ogólnie jest to możliwe, to wiązałoby się z malowaniem tekstu dla obramowania o wiele więcej razy, co może negatywnie odbić się na wydajności renderowania. Mimo wszystko do prostych zastosowań wystarczy – mnie wystarczyło.


W bardzo podobny sposób możliwe jest też malowanie tekstu z cieniem pod spodem. Różnica polega na tym, że tekst cienia wystarczy namalować raz, zamiast cztery razy. To jak daleko ma się znajdować cień i jak duży ma być, można zdefiniować w dodatkowych parametrach. Idąc za ciosem, niżej przykład metody malującej taki tekst:

type
  TCanvasHelper = class helper for TCanvas
  public
    procedure DrawTextShadowed(AX, AY, AOffsetX, AOffsetY: Integer; const AText: String);
  end;
 
  procedure TCanvasHelper.DrawTextShadowed(AX, AY, AOffsetX, AOffsetY: Integer; const AText: String);
  var
    LOldBrushStyle: TBrushStyle;
    LOldFontColor: TColor;
  begin
    LOldBrushStyle := Self.Brush.Style;
    LOldFontColor := Self.Font.Color;
 
    Self.Brush.Style := bsClear;
 
    Self.Font.Color := Self.Pen.Color;
    Self.TextOut(AX + AOffsetX, AY + AOffsetY, AText);
 
    Self.Font.Color := LOldFontColor;
    Self.TextOut(AX, AY, AText);
 
    Self.Brush.Style := LOldBrushStyle;
  end;

I znów – ustawiamy font oraz kolor dodatkowy dla cienia (przykładowo tak jak wcześniej) i wołamy metodę:

Canvas.DrawTextShadowed(PosX, PosY, 2, 2, '4programmers.net');

Efekt działania metody poniżej:

4.png

Co w powiększeniu przedstawia się następująco:

5.png

To nie wszystko – przykład zawiera cień o takim samym rozmiarze jak tekst właściwy, jednak nic nie stoi na przeszkodzie, aby cień był większy lub mniejszy, sprawiając wrażenie głębi (im wizualnie głębiej ma być cień, tym większy i z większym offsetem powinien być malowany).

Można się tym bawić bez końca, więc na cieniu zakończmy ten wpis. ;)

#free-pascal #lazarus

czysteskarpety

bardzo ładnie kawalerze, coś jak logo facebooka :)

furious programming

mogłem wybrać inne kolory – teraz źle się kojarzy… :D

furious programming

Wyrażając sprzeciw co do porównywania moich prac z pewnym nielubianym przeze mnie serwisem, zmieniłem kolorystykę obrazków, aby się ten wpis źle nie kojarzył. Zaktualizowałem też niektóre opisy, aby były jednoznaczne i nie wprowadzały w błąd. W gratisie dorzucam metodę malowania tekstu z cieniem.

Dzięki za feedback. :D

czysteskarpety

nie ma problemu, nie rób sobie wyrzutów :]

Julian_

Tak się napociłeś, a w paincie to byś zrobił to w 1 minutę.

furious programming

Tu nie chodzi o wstawienie obrazków do programu, a o wyrenderowanie tekstu z poziomu kodu.

Kiedy podchodziłem do tego tematu, solidnie przeszukałem sieć pod kątem przykładów. Niestety, ale znalazłem przykłady użycia systemowego API (sposób nie wspierający antialiasingu) oraz takie, które używały zewnętrznych bibliotek. Dlatego też sposób trzeba było znaleźć i stąd ten wpis na blogu – komuś może się to przydać.


Poza tym, w moim projekcie obrazki nie zdałyby egzaminu – wszystkie etykiety muszą być renderowane w locie, dlatego że aplikacja wykorzystuje system motywów i muszą się do niego dostosowywać. To co podałem wyżej faktycznie jest wykorzystywane – dokładnie w takiej postaci. Tekst z obramowaniem wykorzystuję do przycisków zakładek, które posiadają też obrazek z obramowaniem (również renderowany w locie).

Ale renderowanie małej ikonki z obramowaniem to już inna technika – może na kolejny wpis. ;)

Azarien

Jest jeszcze inny sposób: narysować sobie font z pożądanym efektem jako obrazek i w kodzie tylko składać napis z literek. W ten sposób możesz mieć dowolny efekt, nawet taki którego generowanie w locie byłoby powolne.

furious programming

Wcześniej miałem coś takiego, tyle że ogólnie w zastępstwie systemowych mechanizmów – wszystkie kontrolki korzystały z własnoręcznie przygotowanych fontów oraz z klasy umożliwiającej mierzenie i malowanie tekstu na ekranie.

Są z tym dość duże problemy. Jeden z pierwszych to kerning – dość trudno jest wykonać go dobrze i w sposób efektywny. Druga rzecz to obsługa jednego fontu w różnych rozmiarach. Trzeci problem to kolorowanie, a czwarty to przezroczystość. Wyrenderowanie dłuższego tekstu z przezroczystością jest potwornie powolne, wielokrotnie wolniejsze niż renderowanie w sposób standardowy (np. TextOut bez brush-a).

Przerobiłem więc wszystkie kontrolki na obsługę systemowych mechanizmów, a stary kod poszedł do kosza. Chciałem uniknąć instalacji dodatkowych fontów w systemie i sprawić, aby na każdym PC program wyglądał tak samo dobrze. Z drugiej strony chciałem mieć obsługę motywów, co przy własnych fontach zaczynało mocno komplikować całość.

Ale cieszę się, że porzuciłem pomysł używania wałnych fontów – teraz mam ładny i szybko renderowany tekst (również przezroczysty) i obsługę motywów. ;)

Azarien

„potwornie powolne” a jednak TextOut jakoś to robi i robi bardzo szybko :-) Wszystko kwestia optymalizacji kodu i wykorzystania akceleracji sprzętowej. Nie mówię jednak, że własny kod renderujący tekst jest łatwy w napisaniu.

furious programming

Macierz znaków to był zwykły obraz PNG. Każdy font składał się z takiego obrazu, macierzy offsetów, macierzy szerokości znaków i dwóch macierzy marginesów (lewo i prawo – imitowały kerning). Zestaw znaków był nieduży – poniżej 100.

Jeśli font używał obrazu 24-bitowego to bez problemu dało się wypełnić cały ekran tekstem dosłownie w ułamku sekundy. Za kontrolkę testową służył mi mój FormatLabel – wypełniłem go tekstem, ustawiłem Align na alClient i bawiłem się rozmiarem okna. Podczas rozciągania czy kurczenia okna, zawartość kontrolki zmieniała się płynnie, pomimo całego mechanizmu parsowania tekstu i budowania mapy słów przed każdym renderowaniem.

A jak użyłem 32-bitowego, to wyrenderowanie tego samego tekstu na całym ekranie trwało ponad sekundę. O płynnej zmianie rozmiaru komponentu mogłem zapomnieć… :D

furious programming

@Azarien: myślałem że poszedł do kosza, ale jednak przesunałem plik do swojego „archiwum głupich pomysłów”. :]

Cały kod tego menedżera fontów wrzucam tutaj. Jest to ostatnia używana wersja – później usunąłem ten moduł z projektu i nie poprawiałem (a część rzeczy trzeba by jeszcze poprawić czy zoptymalizować).

Metoda odpowiedzialna za właściwe renderowanie tekstu to InternalDrawSingleLineText, w linii 830.

Azarien

szybkie góglanie wykazuje, że ludzie nie mają zbyt pochlebnego zdania na temat wydajności CopyRect, zwłaszcza w połączeniu z przezroczystością. jeśli to na tym tracisz wydajność, spróbowałbym czegoś innego np. funkcji WinApi AlphaBlend (nie wiem jak z jej wydajnością) albo czegoś o czym wiadomo że jest wydajne (Direct3D, OpenGL, czy choćby starego DirectDraw)

furious programming

Porzuciłem ten moduł właśnie ze względu na konieczne modyfikacje i optymalizacje – za dużo z tym roboty, a funkcjonalność zbyt mała. Na pewno CopyRect nie jest najlepszym rozwiązaniem, jednak zależało mi na tym, aby nie babrać się nadmiernie w winapi i nie dociągać dodatkowych bibliotek.

Teraz to nawet nie mam na czym sprawdzić tego AlphaBlend – pliki fontów usunąłem dawno temu, łącznie z konsolowym narzędziem do ich generowania. Pozostanie więc ciekawostką i przestrogą, aby nie być „mądrzejszym od telewizora”.