Опубликован: 08.08.2015 | Доступ: свободный | Студентов: 281 / 38 | Длительность: 09:22:00
Лекция 3:

Анимация

< Лекция 2 || Лекция 3 || Лекция 4 >
Аннотация: Для создания анимированных изображений в настоящей главе применяют-ся предикаты классов windowGDI и GDI+. Предикаты класса windowGDI используются для моделирования движения тела, брошенного под углом к горизонту. Создается класс перемещаемых фигур. Предикаты GDI+ используются для моделирования движения тел по круговым орбитам.

Отображение рисунков. Использование таймера

Создадим форму, в которой перемещаются несколько фигур, а также отображается текущее время. В качестве фона используется рисунок, загруженный из файла. Траектория движения фигуры описывается уравнением движения тела, брошенного под углом к горизонту.

В проекте drawing (см. п. 1.2 "Основные элементы графического интерфейса пользователя" ) следует, как и ранее, создать форму animForm, удалить все кнопки, создать пункт меню Тест -> Анимация, добавить обработчик события выбора этого пункта меню и определить его так, чтобы при выборе данной команды меню открывалась форма animForm.

Подготовка фонового изображения

Подготовим фоновое изображение (например, используя приложение MS Paint) и сохраним его в файле bgpict.bmp в директории Exe проекта (рис. 3.1 рис. 3.1).

Окно "Анимация"

Рис. 3.1. Окно "Анимация"

Способы отображения изображений

Загрузить рисунок из bmp-файла можно с помощью предиката vpi::pictLoad/1:

                  Picture = vpi::pictLoad(FileName).

Для отображения рисунка в окне предназначены предикаты drawPict/3 и drawPict/4 класса windowGDI. Первый из них размещает рисунок, не изменяя его размеры, так чтобы его левый верхний угол находился в заданной точке:

            GDI:pictDraw(Picture, pnt(X, Y), rop_SrcCopy).

Второй предикат берет из рисунка прямоугольный фрагмент и натягивает его на заданную прямоугольную область в окне:

      GDI:pictDraw(Picture, WinRectangle, PictRectangle, rop_SrcCopy).

Ниже применяются оба способа.

Упражнение. Добавьте обработчик события PaintResponder и определите предикат onPaint следующим образом:

onPaint(_Source, _Rectangle, GDI):-
        Picture = vpi::pictLoad("bgpict.bmp"),
        GDI:clear(color_White),
        getClientSize(W, H),
        vpi::pictGetSize(Picture, Wp, Hp, _),
        GDI:pictDraw(Picture, rct(W div 4, H div 4, 3*(W div 4), 3*(H div 4)),
            rct(Wp div 3, 0, 2*(Wp div 3), Hp div 3), rop_SrcCopy).

В прямоугольнике, площадь которого равна четверти площади клиентской области окна, должен появиться фрагмент картины — ее девятая часть.

Создание изображения

Откроем имплементацию класса animForm.pro. Ниже приведено объявление основных параметров, необходимых для построения изображения.

constants
    filename : string = "bgpict.bmp".	 	    % файл с рисунком
    textColor : color = color_SteelBlue.	  % цвет текста
    clockWidth = 100.				                % размер часов
    clockHeight = 40.
    
facts
    width : integer := 400.			            % ширина окна
    height : integer := 300.			          % высота окна
    
    bgpict : picture := erroneous.		      % фоновый рисунок
    bgrect : rct := rct(0, 0, 100, 100).	  % его размер
						                                % шрифт
    font : font := vpi::fontCreateByName("Century Gothic", 14).
    timerClock : timerHandle := erroneous.	% таймер для часов
    timeStr : string := "".			            % строка текста 
    clockRect : rct := rct(0, 0, 10, 10).	  % место для часов
Листинг 3.1. Объявление констант и фактов-переменных

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

predicates
    loadPicture: ().
clauses
    loadPicture():-
        Picture = loadPicture(),
        bgpict := Picture,
        vpi::pictGetSize(bgpict, PictWidth, PictHeight, _),
        bgrect := rct(0, 0, PictWidth, PictHeight).
predicates
    loadPicture: () -> picture.
clauses
    loadPicture() = Picture:-
        try Picture = vpi::pictLoad(filename)
        catch Error do
            stdio::writef("Error %. Unable to load the picture "
                "from %.\n", Error, filename),
            fail
        end try,
        !.
    loadPicture() = Canvas:getPicture():-
        Canvas = pictureCanvas::new(width, height),
        Canvas:clear(color_Aquamarine).
Листинг 3.2. Загрузка фонового изображения

Предикат vpi::pictGetSize/4 возвращает размеры рисунка.

Построение изображения

В окне используется несколько таймеров, один для часов, другие для движущихся фигур. Перерисовывать придется разные фрагменты изображения. Прямоугольник, в котором выводится время, обновляется раз в секунду. Другие обновляемые фрагменты содержат старые местоположения фигур (каждой в отдельности) и новые, в которые фигуры перемещаются за малый промежуток времени. При изменении размеров окна перерисовывается все изображение.

Для того чтобы избежать мерцания, которое может возникнуть из-за частого обновления, изображение строится на холсте. Ниже приведено определение предиката repaint/2, который создает изображение.

predicates
    repaint: (rct, windowGDI).
clauses
    repaint(Rectangle, GDI):-
        not(isErroneous(bgpict)),
        !,
        rct(L, T, R, B) = Rectangle, 
        Width = R - L,
        Height = B - T,
        Canvas = pictureCanvas::new(Width, Height),
        drawPicture(Canvas, Width, Height, Rectangle),
        drawClockText(Canvas, L, T, Rectangle),
        % drawMovingPictures(Canvas, L, T, Rectangle),
        Picture = Canvas:getPicture(),
        GDI:pictDraw(Picture, pnt(L, T), rop_SrcCopy).
    repaint(_Rectangle, _GDI).
Листинг 3.3. Создание фрагмента изображения

Аргументами предиката repaint являются прямоугольник, подлежащий перерисовке, и указатель на объект класса windowGDI.

Предикат drawPicture/4 строит и отображает на холсте Canvas фоновое изображение (см. ниже первое правило) или его фрагмент (см. второе правило). Размеры окна могут меняться, поэтому во втором случае на вспомогательном холсте, размер которого совпадает с текущим размером окна, строится полное изображение, а затем из него берется нужный фрагмент и отображается на холсте Canvas. Предикат drawClockText/4 отображает время, если прямоугольник, в котором выводится время, пересекается с обновляемым прямоугольником. Устанавливается шрифт (setFont/1) и цвет текста (setForeColor/1), затем выводится текст. Предикат drawMovingPictures/4 отображает фигуры, которые хотя бы частично попадают в обновляемый фрагмент. Ниже приведено определение первых двух предикатов. Последний предикат будет определен позднее.

predicates
    drawPicture: (pictureCanvas, integer W, integer H, rct).
clauses
    drawPicture(Canvas, width, height, _Rectangle):- !,
        Canvas:pictDraw(bgpict, rct(0, 0, width, height), bgrect,
            rop_SrcCopy).
    drawPicture(Canvas, W, H, Rectangle):-
        P = pictureCanvas::new(width, height),
        P:pictDraw(bgpict, rct(0, 0, width, height), bgrect,
            rop_SrcCopy),
        Pict = P:getPicture(),
        Canvas:pictDraw(Pict, rct(0, 0, W, H), Rectangle, rop_SrcCopy).
        
predicates
    drawClockText: (pictureCanvas, integer, integer, rct).
clauses
    drawClockText(Canvas, X0, Y0, Rectangle):-
        _ = vpi::rectIntersect_dt(Rectangle, clockRect),
        !,
        rct(L, T, R, B) = clockRect,
        Canvas:setFont(font),
        Canvas:setForeColor(textColor),
        Canvas:drawTextInRect(rct(L – X0, T – Y0, R – X0, B – Y0),
            timeStr, []).
    drawClockText(_Canvas, _X0, _Y0, _Rectangle).
Листинг 3.4. Предикаты построения изображения

Предикат vpi::rectIntersect_dt/2 возвращает пересечение двух прямоугольников. Он истинен, если полученный прямогольник не пуст.

Система координат фрагмента не совпадает с системой координат окна, поэтому выполняется преобразование параллельного переноса на вектор (L, T), где L и T — координаты левой верхней вершины обновляемого прямоугольника.

Запуск таймера

В редакторе формы добавим обработчики событий ShowListener, PaintResponder, EraseBackgroundResponder, SizeListener и TimerListener.

Когда открывается окно, сразу запускаются часы. Кроме этого, загружается фоновое изображение, вычисляются размеры окна, определяется местоположение часов и создаются объекты для перемещаемых фигур (последнее действие будет определено позднее). Ниже определяется предикат onShow.

predicates
    onShow : window::showListener.
clauses
    onShow(_Source, _Data):-
        loadPicture(),
        getClientSize(Width, Height),
        width := Width,
        height := Height,
        % createMovingPictures(),
        clockRect := rct(width - clockWidth, 0, width, clockHeight),
        timeStr := time::new():formatTime("HH:mm:ss"),
        invalidate(),
        timerClock := timerSet(1000).
Листинг 3.5. Определение предиката onShow

Предикат formatTime форматирует текст для вывода времени (см. Help). Предикат timerSet/1 запускает таймер и устанавливает интервал между "тиками" таймера в миллисекундах (1000 = 1 сек).

При изменении размеров окна вычисляется местоположение часов и инициируется обновление всего изображения (см. ниже определение onSize).

clauses
    onSize(_Source):-
        getClientSize(Width, Height),
        width := Width,
        height := Height,
        clockRect := rct(width - clockWidth, 0, width, clockHeight),
        invalidate().
Листинг 3.6. Определение предиката onSize

Определение предиката onPaint содержит вызов предиката repaint.

clauses
    onPaint(_Source, Rectangle, GDI):-
        try repaint(Rectangle, GDI)
        catch _ do
            invalidate(Rectangle)
        end try.
Листинг 3.7. Определение предиката onPaint

Предикат onEraseBackground определяется так же, как и ранее.

clauses
    onEraseBackground(_Source, _GDI) = noEraseBackground.
Листинг 3.8. Определение предиката onEraseBackground

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

predicates
    onTimer : window::timerListener.
clauses
    onTimer(_Source, timerClock):- !,
        timeStr := time::new():formatTime("HH:mm:ss"),
        invalidate(clockRect).
    /*onTimer(_Source, Timer):-
         Pict in movingPictList,
         Timer = Pict:getTimer(),
         !,
         Pict:step().*/
    onTimer(_Source, _Timer).
Листинг 3.9. Определение предиката onTimer

Часы показывают время с точностью до секунды (см. рис. 3.1 рис. 3.1).

Перемещение фигур

Ниже создается класс перемещаемых фигур. Для каждой фигуры создается свой объект этого класса.

Подготовка перемещаемых изображений

Подготовим изображения фигур. Сначала в папке animForm создадим изображения (Bitmap) pict и mask размером 32х32 так, как это было описано в п. 2.1.2 главы 2 "Меню. События мыши" . Перемещаться будет рисунок прямоугольной формы, но пользователь должен видеть перемещение только самой фигуры, фон рисунка должен быть прозрачным. Поэтому для каждой фигуры создается два изображения.

Прозрачная часть рисунка (фон) должна быть в рисунке pict черного цвета (рис. 3.2 (a) рис. 3.2), а в рисунке mask — белого. Непрозрачная часть (фигура) в рисунке mask должна быть черного цвета (рис. 3.2 (b) рис. 3.2). Рекомендуется нарисовать фигуру на белом фоне (pict), затем сделать "маску" (mask) и только после этого закрасить фон первого рисунка (pict) черным цветом.

Изображение (a) pict; (b) mask

Рис. 3.2. Изображение (a) pict; (b) mask

Точно так же подготовим рисунки pict64 и mask64 размером 64х64 для еще одной фигуры (рис. 3.3 рис. 3.3).

Загрузить растровое изображение из файла ресурса (bmp) можно с помощью предиката vpi::pictGetFromRes/1:

Picture = vpi::pictGetFromRes(resourceIdentifiers::idb_pict).

Если загруженные изображения хранятся в фактах-переменных pict и mask, соответственно, то отобразить фигуру можно следующим образом:

        GDI:pictDraw(mask, pnt(100, 100), rop_SrcAnd),
        GDI:pictDraw(pict, pnt(100, 100), rop_SrcInvert).
Исходное расположение фигур

Рис. 3.3. Исходное расположение фигур

Замечание. При перемещении большего числа фигур рекомендуется обновлять все изображение целиком. Для этого потребуется всюду в коде заменить предикат invalidate/1 предикатом invalidate/0. В этом случае построение изображения должно быть существенно упрощено.

Вычисление траектории движения>

Рассчитаем траекторию движения материальной точки. Для простоты в качестве точки возьмем левый верхний угол перемещаемого рисунка.

Пусть r(t) — радиус-вектор точки в момент времени t, тело брошено под углом $\alpha$ к горизонту с начальной скоростью v0 (рис. 3.4 рис. 3.4).

Траектория движения материальной точки

Рис. 3.4. Траектория движения материальной точки

Тело движется с ускорением a = (0, g), где g — величина ускорения свободного падения. Дифференциальное уравнение, описывающее траекторию движения тела, и граничные условия в векторной форме имеют вид:

\ddot{r}=a, \dot{r}(0)=v_{0}, r(0)=r_{0}

Имеем:

\dot{r}(t)=at+v_{0}
r(t)=\frac{at^{2}}{2}+v_{0}t+r_{0}

В скалярной форме:

x(t)=vcos\alpha \cdot t+x_{0}, y(t)=\frac{gt^{2}}{2}-vsin\alpha \cdot t+y_{0}

где v — длина вектора скорости: $v=|v_{0}|$. Для проекций вектора скорости на координатные оси имеем:

v_{x}(t)=vcos\alpha, v_{y}(t)=gt-vsin\alpha

Отметим, что при движении вправо $v_{x}(t)>0$, при движении влево $v_{x}(t) < 0$. Точно так же, при движении вверх $v_{y}(t) < 0$, при движении вниз $v_{y}(t) > 0$.

Величина скорости v и значение угла $\beta$ наклона к горизонту в момент t удовлетворяют соотношениям:

v(t)=\sqrt{v_{x}^{2}(t)+v_{y}^{2}(t)}, tg\beta(t)=\frac{-v_{y}(t)}{v_{x}(t)}

При ударе о горизонтальную поверхность угол $\beta$ меняется на ($-\beta$) при движении вправо и на ($-\beta -\pi$) при движении влево. При ударе о вертикальную поверхность угол ($\beta$) меняется на ($-\beta$) при движении влево и на ($-\beta -\pi$) при движении вправо.

Сопротивление воздуха не учитывается. Масса фигуры полагается равной единице. Будем считать, что абсолютная величина вертикальной проекции вектора скорости после удара о горизонтальную границу окна уменьшается на 10%, а горизонтальной проекции при ударе о вертикальную границу — на 5%.

Класс перемещаемых фигур

Добавим в интерфейс animForm объявление констант и свойств, необходимых для реализации класса перемещаемых фигур.

constants
    g = 9.8.			      % ускорение свободного падения
    deltaT = 0.2.		    % малое приращение времени
properties
    width : integer.		% ширина окна
    height : integer.		% высота окна
Листинг 3.10. Объявление констант и свойств в интерфейсе animForm

Создадим класс перемещаемых фигур movingPicture. В интерфейсе movingPicture объявим свойства, хранящие характеристики изображений и параметры движения, а также следующие предикаты: предикат setInitValues, который устанавливает начальные параметры, предикат step, который рассчитывает перемещение за "тик" таймера, предикат getTimer, предикат timerOnOff, включающий или выключающий таймер, и предикат drawPict, который строит изображение фигуры на холсте.

properties
    id : unsigned.				        % номер рисунка
    pict : vpiDomains::picture.
    mask : vpiDomains::picture.
    size : integer.				        % размер стороны рисунка
    startx : real.				        % начальное положение
    starty : real.
    currentPnt : vpiDomains::pnt.	% текущее положение
    initAngle : real.				      % начальный угол
    initV : real.				          % начальная скорость
    
predicates
    setInitValues: ().	     % устанавливает начальные параметры
    
predicates
    step: ().		      % рассчитывает перемещение за тик таймера
    
predicates
    getTimer: () -> window::timerHandle Timer determ.
    
predicates
    timerOnOff: (vpiDomains::pnt). % включает/выключает таймер
    
predicates
    drawPict: (pictureCanvas, integer, integer, vpiDomains::rct).
Листинг 3.11. Объявление предикатов в интерфейсе movingPicture

В декларации класса объявим конструктор. В нем передается указатель на объект формы animForm, номер рисунка в базе данных, а также идентификаторы ресурсов (bitmap).

constructors
    new:(animForm, unsigned, vpiDomains::resId,vpiDomains::resId).
Листинг 3.12. Объявление контруктора в декларации класса

Ниже описывается имплементация класса movingPicture.

В раздел open имплементации класса необходимо добавить имена классов vpiDomains, math и animForm.

open core, vpiDomains, math, animForm
facts
    id : unsigned.
    pict : picture.
    mask : picture.
    size : integer.
    startx : real := 0.
    starty : real := 0.
    currentPnt : pnt := pnt(0, 0).
    initAngle : real := pi/3.
    initV : real := 40.
    updateRect : rct := erroneous.	% обновляемый прямоуг.
    animForm : animForm.		        % указатель на объект формы
    t : real := 0.			            % параметр времени
    angle : real := 0.			        % угол
    v : real := 0.			            % скорость
    
    timer : window::timerHandle := erroneous.	% таймер
    timerInterval : positive := 20.	% интервал между тиками
    timerOn : positive := 0.		    % включен/выключен таймер
    stopped : boolean := false.  	  % фигура остановилась сама
    
    koefx : real := 0.95.		    % уменьшение скорости при ударе
    koefy : real := 0.9.
    
clauses
    new(Win, Id, PictResId, MaskResId):-
        animForm := Win,
        id := Id,
        pict := vpi::pictGetFromRes(PictResId),
        mask := vpi::pictGetFromRes(MaskResId),
        vpi::pictGetSize(pict, Size, _, _),
        size := Size.
        
clauses
    setInitValues():-
        angle := initAngle,
        v := initV,
        currentPnt := pnt(round(startx), round(starty)).
Листинг 3.13. Основные параметры изображения

Предикат step/0 изменяет значение параметра времени, рассчитывает новое местоположение точки, находит прямоугольник, изображение в котором должно быть изменено, и инициирует обновление изображения в этом прямоугольнике. Предикат step/1 возвращает координаты местоположения точки в момент времени T.

clauses
    step():-
        t := t + deltaT,
        pnt(X, Y) = step(t),
        pnt(X0, Y0) = currentPnt,
        sort(X0, X, MinX, MaxX),
        sort(Y0, Y, MinY, MaxY),
        currentPnt := pnt(X, Y),
        updateRect := rct(MinX, MinY, MaxX + size, MaxY + size),
        animForm:invalidate(updateRect).
        
predicates
    sort: (integer, integer, integer [out], integer [out]).
clauses
    sort(X, Y, X, Y):-
        X < Y,
        !.
    sort(X, Y, Y, X).
    
predicates
    step: (real T) -> pnt NewLocation.
clauses
    step(T) = pnt(round(X), round(Y)):-
        not(isErroneous(timer)),
        !,
        X = restrict(startx + v * cos(angle) * T, 
            0, animForm:width - size),
        Y = restrict(starty - v * sin(angle) * T + g * T^2 / 2, 
            0, animForm:height - size),
        Vx = v * cos(angle),
        Vy = g * T - v * sin(angle),
        Angle = arctan(-Vy/Vx),
        reflection(X, Y, Vx, Vy, Angle),
        stop(Y).
    step(_T) = currentPnt.
Листинг 3.14. Реализация движения

Предикат reflection/5 переустанавливает параметры движения, если рисунок находится вблизи вертикальной или горизонтальной границы окна, для того чтобы фигура "отскочила" от нее. Предикат stop/1 останавливает движение. Движение останавливается в случае, если скорость очень мала и фигура находится вблизи нижней границы окна. Определение этих предикатов приведено ниже.

predicates
    reflection: (real X, real Y, real Vx, real Vy, real Angle).
clauses
    reflection(X, Y, Vx, Vy, Angle):-
        if nearBorder(Y, animForm:height, Vy) then
            reset(X, Y, Vx, Angle, Angle + pi, Vx, Vy, koefy)
        end if,
        if nearBorder(X, animForm:width, Vx) then
            reset(X, Y, Vx, Angle + pi, Angle, Vy, Vx, koefx)
        end if.
        
predicates
    nearBorder: (real Y, integer Height, real Vy) determ.
clauses
    nearBorder(Y, Height, Vy):-
        Y > Height - size - 2, Vy > 0,
        !;
        Y < 3, Vy < 0.
        
predicates
    reset: (real X, real Y, real Vx, real Angle, real AnglePlusPi, real Vx, 
        real Vy, real Koefy).
clauses
    reset(X, Y, V, Angle, Angle1, Vx, Vy, Koefy):-
        v := sqrt(abs(Vx)^2 + abs(Koefy * Vy)^2),
        angle := if V > 0 then - Angle else – Angle1 end if,
        startx := X,
        starty := Y,
        t := 0.
        
predicates
    stop: (real).
clauses
    stop(Y):-
        v < 2.5, Y > animForm:height - size - 2,
        not(isErroneous(timer)),
        !,
        animForm:timerKill(timer),
        timer := erroneous,
        timerOn := 1 - timerOn,
        stopped := true,
        stdio::writef("% stopped\n", id).
    stop(_Y).
Листинг 3.15. Изменение параметров движения

Предикат timerKill/1 останавливает таймер.

Движение начинается и прекращается по двойному щелчку мыши. Для того чтобы его запустить в самом начале, достаточно произвести двойной щелчок кнопкой мыши, когда курсор находится в произвольном месте окна. В дальнейшем запуск и остановка движения производится для каждой фигуры отдельно, курсор во время двойного щелчка кнопки мыши должен указывать на фигуру.

Ниже приведено определение предикатов getTimer и timerOnOff/1. Аргументом последнего предиката является точка касания курсора.

clauses
    getTimer() = timer:-
        not(isErroneous(timer)).
        
clauses
    timerOnOff(Point):-
        not(isErroneous(updateRect)),
        not(vpi::rectPntInside(updateRect, Point)),
        !.
    timerOnOff(_Point):-
        timerOn := 1 - timerOn,
        1 = timerOn,
        !,
        restart(),
        timer := animForm:timerSet(timerInterval).
     timerOnOff(_Point):-
         not(isErroneous(timer)),
         !,
         animForm:timerKill(timer),
         timer := erroneous.
    timerOnOff(_Point).
    
predicates
    restart: ().
clauses
    restart():-
        true = stopped,
        !,
        angle := initAngle,
        v := initV,
        t := 0,
        stopped := false.
    restart().
Листинг 3.16. Предикаты запуска и остановки движения

Если фигура остановилась сама, то факт-переменная stopped принимает значение true (см. выше определение предиката stop). Предикат restart устанавливает исходные значения параметров движения: начальной скорости и угла наклона к горизонту.

Ниже определяется предикат создания изображения.

clauses
    drawPict(Canvas, X0, Y0, Rectangle):-
        (isErroneous(updateRect);
        _ = vpi::rectIntersect_dt(Rectangle, updateRect)),
        !,
        pnt(X, Y) = currentPnt,
        Canvas:pictDraw(mask, pnt(X - X0, Y - Y0), rop_SrcAnd),
        Canvas:pictDraw(pict, pnt(X - X0, Y - Y0), rop_SrcInvert).
    drawPict(_Canvas, _X0, _Y0, _Rectangle).
Листинг 3.17. Создание изображения фигуры

Рисунок отображается только в том случае, если он попадает в обновляемый прямоугольник. Сначала отображается маска, при этом выполняется смешивание цветов фона и маски. После этого отображается сам рисунок, при этом цвета инвертируются.

Управление движением

Вернемся к имплементации класса animForm. Изменим раздел open следующим образом:

        open core, vpiDomains, resourceIdentifiers

Создадим базу данных перемещаемых фигур. Кроме этого, объявим список, в котором будут храниться указатели на объекты класса movingPicture.

% база данных изображений
facts - pictures
    mpict: (unsigned Id, resid Pict, resid Mask).
clauses
    mpict(1, idb_pict, idb_mask).
    mpict(2, idb_pict64, idb_mask64).
    
% список указателей на объекты перемещаемых изображений
facts
    movingPictList : movingPicture* := [].
Листинг 3.18. База перемещаемых фигур

Ниже приведено определение предиката createMovingPictures (в определении предиката onShow следует убрать знак комментария). Для каждой фигуры создается объект класса movingPicture

predicates
    createMovingPictures: ().
clauses
    createMovingPictures():-
        mpict(Id, PictResId, MaskResId),
            Pict = movingPicture::new(This, Id, PictResId, MaskResId),
            K = 2 * math::random(2) - 1,
            Pict:initAngle := K * math::pi/3,
            Pict:initV := K * (math::random(20) + 40),
            Pict:startx := 5 + math::random(3 * (width div 4)),
            Pict:starty := height div 2 - Pict:size,
            Pict:setInitValues(),
            movingPictList := [Pict | movingPictList],
        fail.
    createMovingPictures().
Листинг 3.19. Создание изображений и установка параметров

Координата x начального местоположения фигуры и начальная скорость ее движения определяются случайным образом в указанных пределах.

Ниже определяется предикат drawMovingPictures (в определении предиката repaint также следует убрать знак комментария).

predicates
    drawMovingPictures: (pictureCanvas, integer, integer, rct).
clauses
    drawMovingPictures(Canvas, X0, Y0, Rectangle):-
        Pict in movingPictList,
            Pict:drawPict(Canvas, X0, Y0, Rectangle),
        fail.
    drawMovingPictures(_Canvas, _X0, _Y0, _Rectangle).
Листинг 3.20. Отображение перемещаемых изображений

Движение фигуры начинается или прекращается после двойного щелчка мыши по этой фигуре. Кроме этого, если движение прекратилось само, запустить его заново также можно с помощью двойного щелчка мыши по фигуре.

Добавим и определим обработчик событий MouseDblListener.

predicates
    onMouseDbl : window::mouseDblListener.
clauses
    onMouseDbl(_Source, Point, _ShiftControlAlt, _Button):-
        Pict in movingPictList,
            Pict:timerOnOff(Point),
        fail.
    onMouseDbl(_Source, _Point, _ShiftControlAlt, _Button).
Листинг 3.21. Запуск и остановка движения

Напомним, что в определении предиката onTimer необходимо удалить знаки комментария. Для изменения скорости движения следует использовать предикат timerSet (см. определение предиката timerOnOff).

Движение фигур по круговым орбитам

Ниже создается форма, в которой строится изображение двух планет, вращающихся вокруг солнца. Одна из планет имеет два спутника, другая — два кольца. Планеты и спутники движутся по круговым орбитам. Отображается траектория орбиты одного из спутников. На небе случайным образом размещаются звезды. Звездное небо медленно вращается вокруг центра окна. При каждой перерисовке изображения одна из звезд мерцает. Кроме этого, показывается текущее время с точностью до секунды (рис. 3.5 рис. 3.5). Для создания изображения используются предикаты GDI+. Запустить и остановить движение можно с помощью двойного щелчка мыши, когда курсор находится в произвольной точке окна.

Движение по круговым орбитам

Рис. 3.5. Движение по круговым орбитам

В проекте drawing необходимо создать форму animPlusForm, удалить из нее все кнопки, создать пункт меню Planets ("Движение планет"), добавить обработчик события выбора этого пункта меню и определить его так, чтобы при выборе команды меню Тест -> Движение планет открывалась форма animPlusForm.

Определение основных параметров

В раздел open имплементации класса animPlusForm следует добавить имя класса gdiPlus_native:

open core, vpiDomains, gdiPlus_native

Далее следует изменить определение конструктора new/1, а также добавить обработчик событий DestroyListener и определить его так, как показано ниже (см. п. 1.3.2):

clauses
    new(Parent):-
        formWindow::new(Parent),
        generatedInitialize(),
        % setState([wsf_Maximized]), 
        token := gdiplus::startUp().
        
facts
    token : unsigned := erroneous.
    
predicates
    onDestroy : window::destroyListener.
clauses
    onDestroy(_Source):-
        gdiplus::shutDown(token),
        token := erroneous.

Ниже приведено определение констант и фактов-переменных, хранящих величины углов, на которые поворачиваются тела, и список параметров звезд.

domains
    shape = rectangle; ellipse.
    
constants
    unit : integer = unitPixel.
    
facts
    timerOn : positive := 0. 			        % вкл/выкл таймер
    timerClock : timerHandle := erroneous.		% для часов
    timeStr : string := "".				            % строка времени
    timer : timerHandle := erroneous.			    % для небесн. тел
    starAngle : real := 0.				            % для звезд
    planet1angle : real := 0.				          % для планеты 1
    planet2angle : real := 0.				          % для планеты 2
    angle1 : real := 0.					              % для спутника 1
    angle2 : real := 0.					              % для спутника 2
    starlist : tuple{pnt, integer}* := [].		% звезды
    initWidth : integer := 0.			      % исходная ширина окна
    initHeight : integer := 0. 			    % исходная высота окна
    starNum : positive := 100.		% до 1000 	% число звезд
Листинг 3.22. Основные параметры

Построение изображения

Для того чтобы избежать мерцания из-за частого обновления, изображение строится на холсте. Ниже приведено определение предиката repaint.

predicates
    repaint: (windowGDI).
clauses
    repaint(GDI):-
        getClientSize(Width, Height),
        Canvas = pictureCanvas::new(Width, Height),
        HDC = Canvas:getNativeGraphicContext(IsReleaseNeeded),
        Graphics = graphics::createFromHDC(HDC),
        createDrawing(Graphics, Width, Height),
        Canvas:releaseNativeGraphicContext(HDC, IsReleaseNeeded),
        Picture = Canvas:getPicture(),
        GDI:pictDraw(Picture, pnt(0, 0), rop_SrcCopy).
Листинг 3.23. Создание изображения

Предикат createDrawing/3 создает изображение (см. ниже).

Движения фигур представляют собой композиции поворотов и параллельных переносов (рис. 3.6 рис. 3.6).

Преобразования систем координат: поворот и сдвиг

Рис. 3.6. Преобразования систем координат: поворот и сдвиг

Сначала делается параллельный перенос на такой вектор, чтобы началом координат стал центр окна, выполняется преобразование поворота, затем параллельный перенос на обратный вектор, и отображаются звезды, при этом выполняется преобразование растяжения координат, зависящее от текущего размера окна. Кроме этого, случайным образом выбирается одна звезда и устанавливается ее случайный временный размер. Это создает эффект мерцания звезд.

После этого в исходной системе координат снова выполняется такой сдвиг, чтобы началом координат стал центр окна, и рисуется солнце. Затем в новой системе координат последовательно выполняются преобразования поворота и параллельного переноса, после этого строится изображение первой планеты. В новой системе координат делается то же самое, и сначала отображается орбита первого спутника (толщиной в полпикселя), затем рисуется сам спутник прямоугольной формы. Перед изображением второго спутника восстанавливается предыдущее преобразование (см. код), и так далее.

Изначально центры планет и спутников расположены на оси обсцисс в системе координат, началом которой является центр окна (рис. 3.7 рис. 3.7).

predicates
    createDrawing: (graphics, integer Width, integer Height).
clauses
     createDrawing(Graphics, Width, Height):-
        Graphics:smoothingMode := smoothingModeAntiAlias,
        
    % Фон
        Gradient = linearGradientBrush::createLineBrushFromRectI(
                gdiplus::rectI(0, 0, Width, Height),
                color::create(midnightblue),
                color::create(black),
                linearGradientModeVertical),
        Graphics:fillRectangleI(Gradient, 0, 0, Width, Height),
        
        Transform = Graphics:transform, % запом. исх. преобр.
        
        % Случайный выбор мерцающей звезды
        Nstar = math::random(starNum),
        tuple(Pnt, _) = list::nth(Nstar, starlist),
       
        % Звезды
        Graphics:translateTransform(Width/2, Height/2),
        Graphics:rotateTransform(starAngle),
        Graphics:translateTransform(-Width/2, -Height/2),
        foreach tuple(pnt(Xs, Ys), Sz) in starlist do
            % изменение размеров мерцающей звезды
            StarSize = if pnt(Xs, Ys) = Pnt then 
                2 + math::random(3) else Sz end if,
            % растяжение
            Xstar = math::round(Xs * Width/initWidth),
            Ystar = math::round(Ys * Height/initHeight),
            GrStar = linearGradientBrush::createLineBrushFromRectI(
                gdiplus::rectI(Xstar, Ystar, StarSize, StarSize),
                color::create(white),
                color::create(lightgray),
                linearGradientModeVertical),
            Graphics:fillEllipseI(GrStar, Xstar, Ystar, StarSize, StarSize)
        end foreach,
        
        Graphics:transform := Transform,   % возврат к исх. преобр.
        
        % Часы
        FColor = pen::createColor(color::create(whitesmoke), 1, unit),
        FontFamily = fontFamily::createFromName("Century Gothic"),
        Font = font::createFromFontFamily(FontFamily, 18, 
            fontStyleBold, unit),
        StringFormat = stringFormat::create(0, 0),
        FontRect = gdiplus::rectF(Width - 100, 10, 100, 50),
        Graphics:drawString(timeStr, -1, core::some(Font), FontRect,
            core::some(StringFormat), core::some(FColor:brush)),
            
        % Солнце
        translate(ellipse, Graphics, Width/2, Height/2, 50, yellow, 
            orange, linearGradientModeVertical),
        Transform1 = Graphics:transform,	% запом. преобр. 1
        R = math::min(Width, Height) / 2,
        
        % Планета 1
        rotateTranslate(ellipse, Graphics, planet1angle, 80, 0, 18, 
            lightskyblue, lightseagreen,
            linearGradientModeBackwardDiagonal),
            
        Transform2 = Graphics:transform, 	% запом. преобр. 2
        
        % Орбита спутника 1
        SPen = pen::createColor(color::create(gray), 0.5, unit),
        Graphics:drawEllipseI(SPen, -13, -13, 26, 26),
        
        % Спутник 1
        rotateTranslate(rectangle, Graphics, angle1, 16, 0, 6, white,
            gray, linearGradientModeForwardDiagonal),
            
        % Спутник 2
        Graphics:transform := Transform2, 	% возврат к преобр. 2
        rotateTranslate(ellipse, Graphics, angle2, -24, 0, 8, lightgray,
            blanchedalmond, linearGradientModeHorizontal),
            
        % Планета 2
        Graphics:transform := Transform1, 	% возврат к преобр. 1
        rotateTranslate(ellipse, Graphics, -planet2angle, -R + 30, 0, 30,
            gray, blanchedalmond, linearGradientModeHorizontal),
            
        Graphics:rotateTransform(30),			% поворот
        
        % Кольцо 1
        LPen = pen::createColor(color::create(lightgray), 2, unit),
        Graphics:drawArcI(LPen, -20, -8, 40, 16, 330, 230),
        
        % Кольцо 2
        BPen = pen::createColor(color::create(gray), 2, unit),
        Graphics:drawArcI(BPen, -25, -9, 50, 18, 330, 240).
Листинг 3.24. Построение изображения
Исходное расположение звезд, планет и спутников

Рис. 3.7. Исходное расположение звезд, планет и спутников

Предикат translateTransform/2 выполняет преобразование параллелльного переноса, предикат rotateTransform/1 — преобразование поворота в направлении против часовой стрелки в системе координат окна, т. е. по часовой стрелке для пользователя. Величина угла поворота указывается в градусах.

Например, построим прямоугольник со сторонами 100 и 50, так чтобы его левый верхний угол совпадал с началом координат (0; 0), как показано на рис. 3.8 (a) рис. 3.8 (изменим определение предиката createDrawing; определение предиката onPaint приведено ниже):

Pen = pen::createColor(color::create(black), 2, unitPixel),
Graphics:drawRectangleI(Pen, 0, 0, 100, 50). 
Преобразование системы координат: (a) тождественное; (b) сдвиг; (c) поворот; (d) сдвиг на обратный вектор;

Рис. 3.8. Преобразование системы координат: (a) тождественное; (b) сдвиг; (c) поворот; (d) сдвиг на обратный вектор;

Синим цветом обозначена ось абсцисс, красным — ось ординат.

Добавим преобразование параллельного переноса на вектор (100; 100) (рис. 3.8 рис. 3.8 (b)):

Graphics:translateTransform(100, 100),
Graphics:drawRectangleI(Pen, 0, 0, 100, 50). 

Теперь добавим после преобразования параллельного переноса преобразование поворота на угол 900 (заменим код следующим кодом):

Graphics:translateTransform(100, 100),
Graphics:rotateTransform(90), 
Graphics:drawRectangleI(Pen, 0, 0, 100, 50). 

Оно выполняется уже в новой системе координат, поэтому изображение примет вид, показанный на рис. 3.8 (c) рис. 3.8.

Наконец, добавим преобразование переноса на вектор (–100; –100):

Graphics:translateTransform(100, 100),
Graphics:rotateTransform(90), 
Graphics:translateTransform(-100, -100),
Graphics:drawRectangleI(Pen, 0, 0, 100, 50).

Оно выполняется в текущей системе координат, поэтому начало координат не вернется в точку (0; 0), а окажется в точке (200; 0) (рис. 3.8 (d) рис. 3.8). Проверим это. Матрица преобразования поворота на угол $\alpha$ имеет вид:

\left(\begin{array}{cc}
cos\alpha &-sin\alpha\\
sin\alpha &cos\alpha\\
\end{array}\right)

Композиция преобразований координат (сдвига, поворота на 900 и еще одного сдвига)

{x\choose y}={x_{1}\choose y_{1}}+{100\choose 100}; {x_{1}\choose y_{1}}={0\quad-1\choose 1\qquad0}{x_{2}\choose y_{2}}\quad\mbox{и}\quad {x_{2}\choose y_{2}}={x_{3}\choose y_{3}}+{-100\choose -100}

выглядит следующим образом:

{x\choose y}={0\quad-1\choose 1\qquad0}\left({x_{3}\choose y_{3}}+{-100\choose -100}\right)+{100\choose 100}={200-y_{3}\choose x_{3}}

Таким образом, точка с новыми координатами (0; 0) будет иметь координаты (200; 0) в старой системе координат, т. е. в системе координат окна.

Предикат scaleTransform/2 выполняет преобразование растяжения (сжатия) вдоль осей координат. Если в последней системе координат выполнить преобразование сжатия в два раза вдоль оси абсцисс:

Graphics:scaleTransform(0.5, 1),

то вместо прямоугольника отобразится квадрат со стороной 50. Итоговое преобразование будет выглядеть следующим образом:

{x\choose y}={200-y_{4}\choose 0,5x_{4}}

Предикат rotateTranslate/9 последовательно выполняет сначала преобразование поворота, затем параллельного переноса, после этого отображает фигуру заданной формы (эллипс или прямоугольник). Предикат translate/8 выполняет преобразование параллельного переноса, а затем отображает фигуру. Предикат fillShape/5 отображает фигуру заданной формы. Ниже приведено определение этих предикатов.

predicates
    rotateTranslate: (shape, graphics, real Angle, real ShiftX, 
        real ShiftY, integer Size, unsigned Color1, unsigned Color2, 
        linearGradientMode).
clauses
    rotateTranslate(Shape, Graphics, Angle, ShiftX, ShiftY, Size, 
            Color1, Color2, GradientMode):-
        Graphics:rotateTransform(Angle),
        translate(Shape, Graphics, ShiftX, ShiftY, Size, Color1, Color2,
            GradientMode).
            
predicates
    translate: (shape, graphics, real ShiftX, real ShiftY, integer Size,
        unsigned Color1, unsigned Color2, linearGradientMode).
clauses
    translate(Shape, Graphics, ShiftX, ShiftY, Size, Color1, Color2,
            GradientMode):-
        Graphics:translateTransform(ShiftX, ShiftY),
        A = Size div 2,
        Gradient = linearGradientBrush::createLineBrushFromRectI(
                gdiplus::rectI(- A, - A, Size, Size),
                color::create(Color1),
                color::create(Color2),
                GradientMode),
         fillShape(Shape, Graphics, Gradient, A, Size).
         
predicates
    fillShape: (shape, graphics, linearGradientBrush, integer Pnt, 
        integer Size).
clauses
     fillShape(rectangle, Graphics, Gradient, A, Size):- !,
         Graphics:fillRectangleI(Gradient, - A, - A, A, Size).
     fillShape(_, Graphics, Gradient, A, Size):-
         Graphics:fillEllipseI(Gradient, - A, - A, Size, Size).
Листинг 3.25. Определение местоположения и отображение фигур

Начало и остановка движения

В редакторе формы animPlusForm добавим обработчики событий ShowListener, SizeListener, PaintResponder, EraseBackgroundResponder, MouseDblListener и TimerListener.

Ниже приведено определение предиката onShow. При открытии окна сразу запускаются часы. Случайным образом генерируются параметры "звезд". Запоминаются исходные размеры окна; они используются для вычисления коэффициентов растяжения координат после изменения размеров окна.

clauses
    onShow(_Source, _Data):-
        getClientSize(W, H),
        initWidth := W,
        initHeight := H,
        starlist := [
            tuple(pnt(math::random(W - 5), math::random(H - 5)), 
                1 + math::random(4)) || 
            _ = std::fromTo(1, starNum)],
        timeStr := time::new():formatTime(),
        invalidate(),
        timerClock := timerSet(1000).
Листинг 3.26. Определение предиката onShow

Далее приводится определение остальных обработчиков событий.

clauses
    onPaint(_Source, _Rectangle, GDI):-
        try repaint(GDI)
        catch _ do
            invalidate()
        end try.
Листинг 3.27. Определение предиката onPaint
clauses
    onEraseBackground(_Source, _GDI) = noEraseBackground.
    
Листинг 3.28. Определение предиката onEraseBackground
clauses
    onSize(_Source):-
        invalidate().
Листинг 3.29. Определение предиката onSize

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

clauses
    onMouseDbl(_Source, _Point, _ShiftControlAlt, _Button):-
        timerOn := 1 - timerOn,
        1 = timerOn,
        !,
        timer := timerSet(100).
    onMouseDbl(_Source, _Point, _ShiftControlAlt, _Button):-
        timerKill(timer),
        timer := erroneous.
Листинг 3.30. Определение предиката onMouseDbl
clauses
    onTimer(_Source, timerClock):- 
        timeStr := time::new():formatTime(),
        isErroneous(timer),
        !,
        invalidate().
    onTimer(_Source, timer):- !,
        starAngle := starAngle + 0.05,
        planet1angle := planet1angle + 2.5,
        planet2angle := planet2angle + 0.4,
        angle1 := angle1 + 4,
        angle2 := angle2 - 8,
        invalidate().
    onTimer(_Source, _Timer).
Листинг 3.31. Определение предиката onTimer

Величина скорости движения небесного тела определяется величиной приращения угла поворота (см. определение предиката onTimer).

Если убрать знак комментария из определения конструктора new/1 (см. п. 3.3.1 "Анимация" ), то окно сразу откроется во весь экран (рис. 3.9 рис. 3.9).

Вращение тысячи звезд

Рис. 3.9. Вращение тысячи звезд

Как отобразить рисунок из jpg-файла с помощью предикатов GDI+ показано в примере pictureDraw из собрания примеров Visual Prolog Examples.

Упражнения

2.1. Создайте анимированное изображение движения "бильярдного" шара, в котором направление движения можно было бы изменять с помощью щелчка мыши. Шар должен двигаться в направлении указателя мыши во время щелчка и отскакивать от стенок (границ окна) по законам отражения.

2.2. Реализуйте возможность перетаскивания фигуры с помощью мыши (drag_and_drop).

2.3. Реализуйте перемещение одной фигуры вокруг другой по эллиптической орбите.

< Лекция 2 || Лекция 3 || Лекция 4 >
Владимир Крюков
Владимир Крюков
Казахстан
Berkut Molodoy
Berkut Molodoy
Россия