19 июля 2011

GUI: Стилизованное анимированное меню

Давно не делал демок для журнала. Сегодня решил исправить этот недостаток и сел за Delphi, чтобы продолжить цикл сообщений о GUI. В поисках интересных интерфейсных решений, набрел сюда, но сделать такое за короткое время не удалось. Видимо подзабыл я математику и нужно садиться за листочек бумажки, чтобы накидать основные формулы для такой карусельки. Надеюсь, как-нибудь и такой PageSlider добавится в мою коллекцию GUI-элементов. А сегодня я остановился на анимированной подсветке элементов горизонтального списка. В итоге я скачал несколько иконок и соорудил небольшую демку. В принципе, такой подход может использоваться как для элементов меню, так и для подсветки пунктов в списке апгредов юнита, выбора магии или чего-то подобного.

Результат у нас будет такой:


Так как вся ставка идет на анимации, поэтому записал видео:



Времени было мало, поэтому обкручивать все элементы в отдельные классы не стал и оставил маленький винегрет в исходниках. Если будет свободная минутка - обязательно постараюсь "причесать" код.
Графических элементов на экране не так много - простенький градиентный фон, иконки, полоска-заголовок снизу и текстовые надписи повсюду. Основным являются анимация, переходы между состояниями и реакция на мышь. Так как уже мы делали что-то подобное, то сейчас я коснусь только основных и новых моментов демы.

Кнопкой-иконкой у нас является такая конструкция:

  TItem = class
    fMainHud: TGlHudSprite;
    fTextHud: TGlHudText;
    fBottomText: String;
    fPosition: TVector;
    fY: Single;
    fSelected: Boolean;
    fWidthArea: Single;
  end;

где fMainHud - основное изображение элемента, fTextHud - текст, прячущийся под иконкой, fBottomText - текст, который выезжает слева, fPosition - базовая позиция элемента, fY - single-значение для реализации tween'инга, fSelected - наведена ли мышь?, fWidthArea - можно использовать, если визуальная часть иконки не совпадает с областью реакции на мышь.
Сама структура такого элемента заполняется здесь:

Function TfrmMain.AddItem(aMaterial: String; aText: String; aPosX, aPosY, aWidthArea: Single; aBottomText: String): TItem;
begin
  result := TItem.Create;
  with result do
  begin
    fPosition := VectorMake(80 + aPosX * 70, aPosY, 0);
    
    fTextHud := TGlHudText.CreateAsChild(fMainDummy);
    with fTextHud do
    begin
      BitmapFont := fWinFont;
      ModulateColor.SetColor(0, 0, 0.3, 0.7);
      Alignment := taCenter;
      Text := aText;
      Position.SetPoint(fPosition[0], fPosition[1] - 10, 0);
    end;

    fMainHud := TGlHudSprite.CreateAsChild(fMainDummy);
    with fMainHud do
    begin
      Material.MaterialLibrary := fMatLib;
      Material.LibMaterialName := aMaterial;
      with Material.GetActualPrimaryTexture.Image do
        SetSize(Width, Height);
      Position.SetPoint(fPosition);
    end;

    fBottomText := aBottomText;
    fWidthArea := aWidthArea;
    fY := aPosY;
  end;
  fMenuItems.Add(result);
end;

"Магические числа" нужны для более опрятного вида списка на экране. Поэкспериментируйте, изменив те или иные значения при заполнении элемента списка!
Остальные вспомогательные методы вроде InitTweener(), InitMaterials(), InitFontAndSprites() мы уже разбирали ранее и теперь я просто перейду к описанию основного динамического метода UpdateSelection():

Function TfrmMain.UpdateSelection(x, y: Integer): integer;
var
  i: integer;
begin
  result := -1;
  for i := 0 to fMenuItems.Count - 1 do
    with GetItem(i), GetItem(i).fMainHud do
      if (x > Position.X - Width/2 * fWidthArea)  and (x < Position.X + Width/2 * fWidthArea)
      and(y > Position.Y - fPosition[1] - Height) and (y < fPosition[1] + Height)then
      begin
        result := i;
        if not fSelected then
        begin
          fTweener.DeletePSingle(@fY);
          fTweener.AddTweenPSingle(@fY, ts_ExpoEaseIn, fY, fPosition[1] + 45, 1, 0);
        end;
        fSelected := true;
      end
      else
      begin
        if fSelected then
        begin
          fTweener.DeletePSingle(@fY);
          fTweener.AddTweenPSingle(@fY, ts_ElasticEaseOut, fY, fPosition[1], 2.5, 0);
        end;
        fSelected := false;  
      end;  
end;

Очевидно, что здесь мы проходим по списку элементов и проверяем, наведена ли мышь на один из них. Здесь стоит пояснить новый вызов fTweener.DeletePSingle(@fY), который удаляет все Tween'ы, связанные с адресом переменной. Это необходимо, чтобы прежние анимации не накладывались на новые, что вызывает порой жуткие скачки при движениях элементов. А далее, как обычно:
  • если объект выделен, а на предыдущем кадре нет, тогда запускаем ts_ExpoEaseIn
  • если объект был выделен, а теперь мышь ушла, тогда запускаем ts_ElasticEaseOut в обратном направлении
При клике левой кнопки мыши, запускаются два tween'а на движение панельки и текста внизу:

   if IsKeyDown(vk_LButton) and not MousePressed and (Selected <> -1)then
  begin
    fMenuText.Text := GetItem(Selected).fTextHud.Text;
    fBottomText.Text := GetItem(Selected).fBottomText;

    fTweener.DeletePSingle(@fTextBarX);
    fTweener.AddTweenPSingle(@fTextBarX, ts_ExpoEaseIn, -500, 250, 1.5, 0);

    fTweener.DeletePSingle(@fBottomTextX);
    fTweener.AddTweenPSingle(@fBottomTextX, ts_ExpoEaseIn, -400, 80, 1.5, 0.4);
  end;

А сами позиции применяются простым присвоением:

  fTextBar.Position.X := fTextBarX;
  fMenuText.Position.X := 530 - fTextBarX;
  fBottomText.Position.X := fBottomTextX;

Можно заметить, что панелька fTextBar и сам текст fMenuText движутся благодаря одному и тому же tween-элементу, но выползают они с разных сторон окна, что придает их движению разнообразия.
Наверно это все, что можно пояснить в тексте... какие-то отдельные моменты наверно все же придется смотреть в коде, чтобы понять более детально устройство самой демы...
Само демонстрационное приложение можно скачать здесь, либо воспользуйтесь svn для выкачивания сразу всех примеров, написанных мною. Данная демка добавилась в 9ой ревизии.

p.s. Последний месяц для fun'а я занимался только конкурсом, поэтому журнал не наполнялся gui-вкусностями и различными демками. Через месяц-другой, как станет посвободнее, обязательно вернусь к славной традиции добавления новых интерфейсных элементов на страницы журнала. Ну а пока в следующих записях буду рассказывать о текущих проектах, сложностях в познании flash, результатах конкурса, создании своего сайта и прочего... Следите за новостями!

Сообщения, схожие по тематике:

10 коммент.:

  1. Обновился через SVN.
    Спасибо за демку:)
    попробую использовать tween без привязки к сцене.

    p.s. стоит хотя бы для того, что бы поиграть с этим самому)

    ОтветитьУдалить
  2. я в p.s. забыл слово "скачать"

    ОтветитьУдалить
  3. Спасибо, товарищ) Правильный метод добавили - delete, без него иногда у меня тоже были жуткие скачки)

    Как будет время - тоже обновлюсь с SVN, чтобы заценить весь код)

    Все никак руки не дойдут написать свой SimpleButton и tween-инг на основе твоих, немного подправив под мои, местами изогнутые руки (естественно, с оставлением записей "based on lampogolovii's GUI projects [www]").

    ОтветитьУдалить
  4. Красиво сделал, молодец :) Не думал на твинер добавить событие окончания анимации(аналогично Caurina transitions или Tweener на Flash)?
    А что сложного в примере, на который ты даешь линк в начале? На сцене со стандартыми HUD-cпрайтами только размытие будт сложно сделать(и то, если не использовать хотя бы 120 GLSL). С шейдерами - просто делаем каждый спрайт раза в 2-1.5 больше картинки(так как размер размытой картинки будет больше - при том же размере спрайта края размытия будут "обрезаться"), размываем с использованием фильтра Гаусса.

    ОтветитьУдалить
  5. Ulop, рад, что понравилось!

    perfect daemon, да, при комплексных твинингах проблема отсутствия Delete-методов у меня остро встала! если будут еще какие предложения по улучшению/дополнению - смело высказывай, постараюсь обязательно добавить!

    C4, благодарю!
    насчет событий - наверно стоит развернуть событийную структуру посильнее, согласен! обязательно добавлю, благодарю за наводку!

    по поводу PageSlider'а - элементы нужно нелинейно двигать по прямой, перебрасывать из начала в конец и манипулировать прозрачностью... в общем, что-то у меня голова пошла кругом и я забил...
    размытие по боку, для начала хватит одной прозрачности! :)

    ОтветитьУдалить
  6. L
    Насчет "перебрасывать из начала в конец".
    Мне как-то давно в школе показали один интересный прием, о котором я никогда бы сам не подумал.

    //Надо выбрать следующий
    Inc(index);
    index := index mod Count;

    Для предыдущего схожий подход :)

    ОтветитьУдалить
  7. Все очень красиво (ну, визуально, по крайней мере), но область применения, ИМХО, ограниченная. Код пока не смотрел.

    Надо будет, когда руки дойдут, сделать нормальный класс для перехвата мыши\клавиатуры, чтобы для разных подсистем один был.

    ОтветитьУдалить
  8. perfect daemon, там все немного сложнее... дело в том, что 0-элемент (это тот, который четко виден и как бы является выделенным) при смене на следующий плавно приближается к нам с уменьшением альфы...
    итого так: вдалеке маленькая альфа, при выделении альфа=1, при приближении альфа опять уменьшается...
    там наверно 3-4 if'а должно быть... проще запрогать, чем пояснить словами, наверно))).. но за идею спасибо - она пригодится чуть-чуть в другом месте!

    Марцелл, по поводу области применения - я же не предлагаю копи-пастить себе в проект)).. можно считать, что это непрокручиваемый список, а его использование может быть где угодно: выбор заклинания, выбор следующего апгрейда, выбор сложности игры (easy-medium-hard), выбор расы персонажа, переключение между воинами отряда и т.д... мне наоборот казалось, что область применения широка!
    по поводу класса мыши+клавиатуры - до нормального GUI мне вообще еще далеко: нужно выделить отдельно анимацию, отдельно мышь, отдельно события+скрипты, отдельно звук и т.д... но я буду стараться сделать опрятный GUI!

    ОтветитьУдалить
  9. По-моему не удачно нарисовано в психологическом плане: я навожу мышь на кнопку, а она тут же отъезжает вниз на значительное расстояние. Если хочется, чтобы кнопка отъезжала, сделай чтобы это происходило не более чем на три-четыре пикселя. Но никак не на 50% своей высоты. Иначе стрёмно: кнопка уезжает так далеко, что указатель мыши уже и не на кнопке, и не понятно, нажму я её, если теперь нажму, или не нажму

    ОтветитьУдалить
  10. Привет, отличная демка, спасибо! Если не сложно, можешь сделать демку горизонтально прокручивающегося меню. Буду очень признателен!

    ОтветитьУдалить