суббота, 1 января 2011 г.

Добавление графики нашему механизму

Расширим предыдущий пример, добавив к нему графику и дополнительные возможности.
Код данного примера будет довольно сильно переработан.
1. Чтобы добавить графику, нужно сначала ее нарисовать. Здесь я не стал сильно мудрить, а просто взял картинку из предыдущего поста и разбил ее на несколько частей:
Корпус двигателя
Коленвал с маховиком
Шатун
Поршень

Сохранять эти изображения нужно в формате png, поддерживающем прозрачный фон.

2. Создаем новый AS3 проект во FlashDevelop, задавая размеры выходного файла равными 192х410 пикселей. Переносим файлы изображений деталей двигателя в папку lib нашего проекта.

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

3.1. Сначала разберемся с кодом модели механизма, за нее будет отвечать класс MechModel:
package  
{
 import flash.events.Event;
 import flash.events.EventDispatcher;
 import flash.geom.Point;
 
 // Данный класс мы наследуем от EventDispatcher, 
 // т.к. нам нужно чтобы он мог создавать события 
 // (как бы «выкрикивать» во внешний мир 
 // «Эй, интересно вам или нет, но я изменился»), 
 // а другие классы, подписанные на эти события 
 // могли бы как-нибудь на это реагировать
 // (например, изменять отображение механизма на экране) 
 public class MechModel extends EventDispatcher
 {
  // Здесь мы объявляем константу события, 
  // создаваемого данным классом
  public static const MODEL_CHANGED:String = "modelChanged";
  
  // Основные размеры механизма:
  // Длина кривошипа АВ
  private var a:Number;
  // Длина шатуна ВС
  private var b:Number;
  
  // Угловые параметры, характеризующие положение механизма:
  // Угол поворота кривошипа
  private var _phi:Number = 0;
  // Угол поворота шатуна
  private var _psi:Number;

  // Угловая скорость кривошипа
  public var angularVelocity:Number = 0.1;
  
  // Положения основных точек механизма
  public var pointA:Point;
  public var pointB:Point;
  public var pointC:Point;
  
  public function MechModel(a:Number = 50, b:Number = 200) 
  {
   pointA = new Point(0, 0);
   pointB = new Point();
   pointC = new Point();
   this.a = a;
   this.b = b;
   _phi = 0;
   calculatePositions();
  }
  
  // Обновляем положение точек механизма
  public function update():void
  {
   // Обновление значения угла поворота кривошипа
   _phi += angularVelocity;
   calculatePositions();
  }
  
  // Расчет текущего положения механизма 
  // в зависимости от угла поворота кривошипа.
  private function calculatePositions():void
  {
   pointB.x = pointA.x + a * Math.cos(_phi);
   pointB.y = pointA.y + a * Math.sin(_phi);
   _psi = Math.asin(a * Math.sin(_phi ) / b);
   var ac:Number = a * Math.cos(_phi ) + b * Math.cos(_psi);
   pointC.x = pointA.x + ac;
   pointC.y = pointA.y;
   
   // Т.к. положение механизма изменилось, то надо
   // “оповестить” всех тех, кому это “интересно”:
   dispatchEvent(new Event(MODEL_CHANGED));
  }
  
  // Данный метод пришлось добавить из-за появления 
  // ошибки при подготовке этого урока.
  // Если брать в качестве угла поворота кривошипа 
  // просто значение угла _phi 
  // (а не остаток от его деления на 2 пи), то через 
  // некоторое время после запуска приложения 
  // переставал вращаться кривошип.
  // Видимо, во флэше существует ограничение на значение 
  // свойства rotation 
  public function get phi():Number { 
   return _phi % (2 * Math.PI); 
  }
  
  public function get psi():Number { 
   return _psi; 
  } 
 }
} 
Как видно, из данного класса было удалено все, что связано с отображением объектов на экране. В нем хранится лишь информация о положении точек и метод для обновления их положения.

3.2. В этом примере мы увидим, что одно и то же явление можно увидеть по-разному (у одной модели может быть несколько видов), для этого создадим 2 класса видов: один с использованием векторной графики (как в предыдущем уроке), а второй – с растровой графикой, подготовленной мной заранее.
Начнем с более простого класса, т.к. его код мы уже частично знаем из предыдущего урока. Его мы будет использовать в качестве “тестового” режима отрисовки:
package 
{
 // Импорт необходимых классов
 import flash.display.Bitmap;
 import flash.display.Sprite;
 import flash.events.Event;
 
 // Объявление класса
 public class MechViewVector extends Sprite 
 {
  
  // Ссылка на модель отрисовываемого механизма
  private var model:MechModel;
  
  // Символы для отображения характерных точек механизма
  //
  // Точка А
  public var sharnirOne:Sprite;
  // Точка В
  public var sharnirTwo:Sprite;
  // Точка С
  public var sharnirThree:Sprite;
  // Ползун
  private var polzun:Sprite;
  // Конструктор класса
  public function MechViewVector(model:MechModel):void 
  {
   this.model = model;
   
   initView();
   
   // Добавление обработчика события изменения 
   // модели механизма
   model.addEventListener(MechModel.MODEL_CHANGED,
      modelChangedHandler);
  }
  
  // Построение начального положения механизма
  private function initView():void
  {
   sharnirOne = makeSharnir(model.pointA.x, model.pointA.y);
   addChild(sharnirOne);
   sharnirTwo = makeSharnir(model.pointB.x, model.pointB.y);
   addChild(sharnirTwo);
   polzun = makePolzun(model.pointC.x, model.pointC.y);
   addChild(polzun);
   sharnirThree = makeSharnir(model.pointC.x, model.pointC.y);
   addChild(sharnirThree);
   
   render();
  }
  
  // Обработчик события, вызываемый при изменении модели
  private function modelChangedHandler(e:Event):void 
  {
   render();
  }
  
  // Отрисовка текущего положения механизма
  private function render():void
  {
   // Обновление положения точки A:
   sharnirOne.x = model.pointA.x;
   sharnirOne.y = model.pointA.y;
   
   // Обновление положения точки В:
   sharnirTwo.x = model.pointB.x;
   sharnirTwo.y = model.pointB.y;
   
   polzun.x = model.pointC.x;
   polzun.y = model.pointC.y;
   // Обновление положения точки С:
   sharnirThree.x = model.pointC.x;
   sharnirThree.y = model.pointC.y;
   // Очистка экрана
   this.graphics.clear();
   this.graphics.lineStyle(2, 0x000000);
   // Отрисовка звеньев АВ и ВС:
   drawStergen(sharnirOne, sharnirTwo);
   drawStergen(sharnirTwo, sharnirThree);
  }
  
  // Отрисовка ползуна
  private function makePolzun(x:Number, y:Number):Sprite
  {
   var polzun:Sprite = new Sprite();
   polzun.graphics.lineStyle(2, 0x000000);
   polzun.graphics.beginFill(0x0099ff);
   polzun.graphics.drawRect( -15, -8, 30, 16);
   polzun.graphics.endFill();
   polzun.x = x;
   polzun.y = y;
   return polzun;
  }
  
  // Отрисовка стержневых звеньев
  private function drawStergen(spr1:Sprite, spr2:Sprite):void
  {
   this.graphics.moveTo(spr1.x, spr1.y);
   this.graphics.lineTo(spr2.x, spr2.y);
  }
  
  // Отрисовка характерных точек механизма
  private function makeSharnir(x:Number, y:Number):Sprite
  {
   var sharnir:Sprite = new Sprite();
   sharnir.graphics.lineStyle(2, 0x000000);
   sharnir.graphics.beginFill(0x0099ff);
   sharnir.graphics.drawCircle(0, 0, 4);
   sharnir.graphics.endFill();
   sharnir.x = x;
   sharnir.y = y;
   return sharnir;
  } 
 }
}
Единственной новой деталью является добавление классу свойства model (ссылка на модель механизма), необходимое для своевременного информирования об изменении в модели механизма.

Во втором классе вида мы, наконец, будем использовать подготовленную для этого урока графику. Если Вы еще не умеете встраивать собственную графику с помощью FlashDevelop, то рекомендую почитать статью “Где спрятана библиотека FlashDevelop и как ей пользоваться?”. Код нашего второго класса вида:
package  
{
 import flash.display.Bitmap;
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.geom.Point;
 
 public class MechViewRaster extends Sprite
 {
  
  // Графика для нашего механизма
  [Embed(source = '../lib/flywheel.png')]
  private var FlywheelPNG:Class;
  [Embed(source = '../lib/housing.png')]
  private var HousingPNG:Class;
  [Embed(source = '../lib/piston.png')]
  private var PistonPNG:Class;
  [Embed(source = '../lib/stone.png')]
  private var StonePNG:Class;
  
  // Спрайты для элементов механизма
  public var housingSpr:Sprite;
  public var flywheelSpr:Sprite;
  public var pistonSpr:Sprite;
  public var stoneSpr:Sprite;
  
  // Регистрационные точки для графических элементов механизма 
  private var housingRegPoint:Point = new Point(70, 94);
  private var stoneRegPoint:Point = new Point(25, 44);
  private var flywheelRegPoint:Point = new Point(50, 48);
  private var pistonRegPoint:Point = new Point(34, 30);
  
  // Ссылка на модель механизма
  private var model:MechModel;
  
  public function MechViewRaster(model:MechModel) 
  {
   this.model = model;
   
   model.addEventListener(MechModel.MODEL_CHANGED, 
      modelChangedHandler);
   
   // Создание графики для деталей механизма
   //
   // Корпус двигателя
   var housingBmp:Bitmap = new HousingPNG();
   housingSpr = new Sprite();
   housingSpr.addChild(housingBmp);
   housingBmp.x = -housingRegPoint.x;
   housingBmp.y = -housingRegPoint.y;
   addChild(housingSpr);
   
   // Коленвал с маховиком
   var flywheelBmp:Bitmap = new FlywheelPNG();
   flywheelBmp.smoothing = true;
   flywheelSpr = new Sprite();
   flywheelBmp.x = -flywheelRegPoint.x;
   flywheelBmp.y = -flywheelRegPoint.y;
   flywheelSpr.addChild(flywheelBmp);
   addChild(flywheelSpr);
   
   // Шатун
   var pistonBmp:Bitmap = new PistonPNG();
   pistonBmp.smoothing = true;
   pistonSpr = new Sprite();
   pistonBmp.x = -pistonRegPoint.x;
   pistonBmp.y = -pistonRegPoint.y;
   pistonSpr.addChild(pistonBmp);
   addChild(pistonSpr);
   
   // Рисуем маркер регистрационной точки шатуна 
   // (просто для примера)
   drawMark(pistonSpr);
   
   // Поршень
   var stoneBmp:Bitmap = new StonePNG();
   stoneSpr = new Sprite();
   stoneBmp.smoothing = true;
   stoneBmp.x = -stoneRegPoint.x;
   stoneBmp.y = - stoneRegPoint.y;
   stoneSpr.addChild(stoneBmp);
   addChild(stoneSpr);
   
   render();
  }
  
  private function modelChangedHandler(e:Event):void 
  {
   render();
  }
  // Отрисовка текущего положения механизма
  private function render():void
  {
   housingSpr.y = model.pointA.y;
   housingSpr.x = model.pointA.x;
   housingSpr.rotation = 0;
   
   flywheelSpr.y = model.pointA.y;
   flywheelSpr.x = model.pointA.x;
   flywheelSpr.rotation = model.phi * 180 / Math.PI;
    
   pistonSpr.x = model.pointB.x;
   pistonSpr.y = model.pointB.y;
   pistonSpr.rotation = -model.psi * 180 / Math.PI;
    
   stoneSpr.y = model.pointC.y;
   stoneSpr.x = model.pointC.x;
   stoneSpr.rotation = 0;
  }
  
  // Функция для отрисовки маркера (для тестовых нужд)
  private function drawMark(spr:Sprite):void
  {
   var markSpr:Sprite = new Sprite();
   markSpr.graphics.lineStyle(2, 0x000000);
   markSpr.graphics.moveTo( -15, 0);
   markSpr.graphics.lineTo(15, 0);
   markSpr.graphics.moveTo(0, -15);
   markSpr.graphics.lineTo(0, 15);
   spr.addChild(markSpr);
  }
 }
}
Самым интересным, на мой взгляд, моментом в данном классе является использование так называемых регистрационных точек. Т.к. у экземпляров класса Bitmap (коими и являются встроенные нами графические файлы) регистрационные точки находятся в верхнем левом углу, то работа по их позиционированию на экране становится довольно сложной задачей. Для упрощения этой проблемы мы создаем своеобразные “обертки” для графических элементов и уже внутри их размещаем нашу графику. Например, “центром” корпуса двигателя является точка, через которую проходит ось коленвала, на рисунке видны ее координаты. В коде рассматриваемого класса эта точка представлена следующей строкой:
private var housingRegPoint:Point = new Point(70, 94);

Найти координаты “регистрационной” точки можно воспользовавшись любым графическим редактором, даже Paint подойдет :))

//В конструкторе мы создаем графический элемент для корпуса
var housingBmp:Bitmap = new HousingPNG();
// Затем “оборачиваем” его внутрь созданного специально 
// для этого спрайта
housingSpr = new Sprite();
housingSpr.addChild(housingBmp);
// А потом размещаем его внутри этого спрайта
housingBmp.x = -housingRegPoint.x;
housingBmp.y = -housingRegPoint.y;
// И добавляем в список отображения не сам объект Bitmap, 
// а спрайт
addChild(housingSpr);
Также мы поступаем с остальными элементами.

3.3. Модель и виды у нас готовы, теперь осталось собрать это воедино. Данную роль будет выполнять класс Main (он же основной класс проекта):
package  
{
 import com.bit101.components.PushButton;
 import flash.display.Sprite;
 import flash.display.StageAlign;
 import flash.display.StageScaleMode;
 import flash.events.Event;
 import flash.events.MouseEvent;
 import flash.geom.Point;
 
 public class Main extends Sprite
 {
  
  private var model:MechModel;
  
  private var vectorView:MechViewVector;
  private var rasterView:MechViewRaster;
  
  // Логический “флаг”, говорящий о том, работает ли механизм
  // в данный момент
  private var running:Boolean = false;
  
  private var pButton:PushButton;
  
  public function Main() 
  {
   stage.align = StageAlign.TOP_LEFT
   stage.scaleMode = StageScaleMode.NO_SCALE;
   
   // Создаем модель и виды для данной модели
   model = new MechModel(20, 120);

   vectorView = new MechViewVector(model);
   vectorView.x = stage.stageWidth / 2;
   vectorView.y = 305;
   vectorView.rotation = -90;
   // Изначально тестовый вид не отображается
   // Его можно включить, щелкнув мышью по сцене
   vectorView.visible = false;
   
   rasterView = new MechViewRaster(model);
   rasterView.x = stage.stageWidth / 2;
   rasterView.y = 305;
   rasterView.rotation = -90;
   
   addChild(rasterView);
   addChild(vectorView);
   
   // Кнопка запуска-остановки механизма
   pButton = new PushButton(this, 10, 385, "Play", onClick);
   pButton.width = 172;
   
   // Добавляем возможность включения-выключения
   // тестового режима отрисовки механизма
   stage.addEventListener(MouseEvent.CLICK, clickHandler);
  }
  
  protected function onClick(event:Event):void
  {
   running = !running;
   if(running)
   {
    event.target.label = "Stop";
    addEventListener(Event.ENTER_FRAME, enterFrameHandler);
   }
   else
   {
    event.target.label = "Play";
    removeEventListener(Event.ENTER_FRAME, enterFrameHandler);
   }
   // Данная строка нужна для того, чтобы событие 
   // щелчка мышью не распространялось дальше 
   // по списку отображения
   // Если эту строку закомментировать, то при щелчке
   // мышью по кнопке будет включаться и выключаться 
   // тестовый режим отрисовки механизма, 
   // что является нежелательным поведением
   event.stopPropagation();
  }
  
  // Обновление состояния модели
  private function enterFrameHandler(e:Event):void 
  {
   model.update();
  }
  
  // Включение-отключение тестового режима отрисовки
  private function clickHandler(e:MouseEvent):void 
  {
   vectorView.visible = !vectorView.visible;
  }
 }
}
Для управления работой двигателя я добавил специальную кнопку pButton, являющуюся экземпляром класса PushButton, данный класс входит в библиотеку minimalcomps, включающую в себя и другие элементы пользовательского интерфейса (текстовые поля, чекбоксы, переключатели и т.д.). Эта библиотека проста в использовании и довольно полезна при разработке прототипов (ускоряя ее). Думаю, что в следующих уроках опишу подробнее ее применение.

Это был последний класс в этом примере, откомпилировав его Вы должны получить что-то вроде этого:


Исходники к уроку

После этого урока мы немного отвлечемся от построения моделей механизмов и сделаем упор на векторную математику и тригонометрию, а также рассмотрим довольно интересную математическую библиотеку.
Ну и конечно: С наступившим Новым Годом!

Комментариев нет:

Отправить комментарий