Łączenie dwóch obrazów w jeden na podstawie zadanego poziomu

9

Dłubię sobie przy swoim projekcie aplikacji dla Tetrisa i jestem na etapie programowania paska stanu, który docelowo ma wyświetlać krótkie tekstowe informacje (niektóre dodatkowo z małymi obrazkami). Jego zawartość ma się płynnie zmieniać, a sama zmiana wyświetlanej etykiety ma wykorzystywać animowany efekt przejścia – obecny obraz ma płynnie przechodzić w drugi.

Jeśli by ktoś potrzebował kodu łączącego dwa obrazy (bez kanału alfa, np. 24-bitowe bitmapy) w jeden na podstawie poziomu z zadanej skali to niżej podaję przykładowy kod źródłowy procedury łączącej, a w załącznikach źródła testowego projektu dla Lazarusa oraz plik wykonywalny do szybkiego kuknięcia o co chodzi.


Do wykonania ww. czynności potrzebne są dwa obrazy źródłowe oraz jeden docelowy – dla uproszczenia użyłem wszystkich o takim samym rozmiarze. Potrzebna jest też wartość poziomu oraz maksimum skali – dla przykładu posłużę się procentami.

Im mniejszy procent, tym bardziej widoczny będzie pierwszy obraz źródłowy, im wyższy tym drugi. W przypadku wartości brzegowych (minimum i maksimum skali) obraz docelowy będzie taki sam jak pierwszy obraz źródłowy (minimum) lub drugi (maksimum).

procedure CombineImages(AImageA, AImageB, AImageDest: TPortableNetworkGraphic; ALevel, AScale: UInt8);
type
  PPNGPixel = ^TPNGPixel;
  TPNGPixel = record B, G, R, A: UInt8; end;
type
  PPNGLine = ^TPNGLine;
  TPNGLine = array [UInt16] of TPNGPixel;
var
  LineA, LineB, LineDest: PPNGLine;
  PixelA, PixelB, PixelDest: PPNGPixel;
var
  LineIndex, PixelIndex: Integer;
begin
  AImageDest.BeginUpdate();
  try
    for LineIndex := 0 to AImageA.Height - 1 do
    begin
      LineA := AImageA.ScanLine[LineIndex];
      LineB := AImageB.ScanLine[LineIndex];
      LineDest := AImageDest.ScanLine[LineIndex];

      for PixelIndex := 0 to AImageA.Width - 1 do
      begin
        PixelA := @LineA^[PixelIndex];
        PixelB := @LineB^[PixelIndex];
        PixelDest := @LineDest^[PixelIndex];

        PixelDest^.R := Round(PixelA^.R + (PixelB^.R - PixelA^.R) / AScale * ALevel);
        PixelDest^.G := Round(PixelA^.G + (PixelB^.G - PixelA^.G) / AScale * ALevel);
        PixelDest^.B := Round(PixelA^.B + (PixelB^.B - PixelA^.B) / AScale * ALevel);
      end;
    end;
  finally
    AImageDest.EndUpdate();
  end;
end;

Wynik działania prezentuje poniższy zrzut:

combine-images.png

Do łączenia dwóch kolorów wykorzystałem te same obliczenia, których użyłem w swoim platformerze do płynnej zmiany koloru tła w animacji staffu – jakiś czas temu pisałem o tej animacji na blogu, więc jeśli ktoś chce zobaczyć w czym rzecz to klikać w link – w dalszej części wpisu jest link do filmiku. ;)

2

Napisałem sobie testowy program do sprawdzenia jak będzie wyglądać i zachowywać się cały animowany baner.

Wszystko działa elegancko, tak jak przypuszczałem. Zmiana obrazków w banerze działa płynnie i nie powoduje żadnego zauważalnego wzrostu zużycia mocy CPU, nawet na moim 12-letnim trupie – menedżer zadań cały czas pokazuje okrągłe 0% zużycia, co mnie oczywiście zadowala. :)

animated banner.gif

Gif trochę się przycina, no ale widać w czym rzecz. W razie czego demówka jest w załącznikach.


Dla testu skorzystałem ze zwykłego timera – żadnych problemów, pasek jest odmalowywany nawet jeśli wyświetli się okno modalne czy w momencie przesuwania okna po ekranie. Tak więc nie potrzeba tutaj wątków pobocznych, bo implementacja jest banalna i nie ma co komplikować sobie życia. Przy czym w timerze zastosowałem zmienny interwał – 2500ms oczekiwania na przejście oraz 50ms pomiędzy klatkami przejścia – dzięki czemu przez całe dwie i pół sekundy baner nie jest odmalowywany w ogóle.

Obsługa przejść i odmalowywania płótna okna jest króciutka:

procedure TMainForm.UpdateBanner(ASender: TObject);
var
  BannerA, BannerB: TPortableNetworkGraphic;
begin
  FTransitionFrameIndex += 1;

  if FTransitionFrameIndex <= BANNER_TRANSITION_FRAMES then
  begin
    BannerA := FBanners[FBannerAIndex];
    BannerB := FBanners[FBannerBIndex];

    CombineBanners(BannerA, BannerB, FBannerBuffer, FTransitionFrameIndex, BANNER_TRANSITION_FRAMES);
    UpdateBannerCanvas();

    FTimer.Interval := BANNER_INTERVAL_TRANSITION
  end
  else
  begin
    FTransitionFrameIndex := 0;

    FBannerAIndex := (FBannerAIndex + 1) mod FBanners.Count;
    FBannerBIndex := (FBannerBIndex + 1) mod FBanners.Count;

    FTimer.Interval := BANNER_INTERVAL_VISIBLE;
  end;
end;

Cały kod znajduje się w archiwum dołączonym do załączników, aplikacja wykonywalna także – można się pobawić.

Teraz skoro już wiem, że wszystko działa super, pozostanie mi opakowanie całej logiki w konkretne klasy i dodanie opcji konfiguracji jego zachowania, a na koniec dołączenie ich do głównego projektu. ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1