воскресенье, 30 января 2011 г.

TMM и Flash

Для тех, кто не в курсе ТММ - это теория механизмов и машин - "дисциплина, которая изучает строение (структуру), кинематику и динамику механизмов в связи с их анализом и синтезом".
Как же можно связать Flash и ТММ? Первое, что приходит на ум - это показать работу рассматриваемых в курсе данной дисциплины механизмов, что и сделал до меня товарищ Werdn вот здесь (там же можно найти и исходники). Его программа реализована на haXe, я же решил перевести его труды на as3.

В программе реализованы следующие структурные группы второго класса:
- twolevers - структурная группа, состоящая из 2-х звеньев, соединенных цилиндрическим шарниром;
- kulisa - структурная группа, состоящая из коромысла (рычаг с направляющей) и камня (звено, поступательно двигающееся по направляющей коромысла);
- crowler - структурная группа "шатун-ползун".
Благодаря наличию группы splitter возможно соединение рычажных звеньев не только их конечными точками.
А также механизм нулевого класса - кривошип (к нему прикладывается вращательный момент).
На данный момент в программе возможно создание подвижных моделей плоских рычажных механизмов с одной степенью свободы, состоящих из кривошипа присоединенных к нему структурных групп.
Программа на входе принимает xml-документ, обрабатывает его содержимое и на основе этого строит модель механизма.
Исходники на as3 можно взять здесь. Описание структуры и примеры xml-файлов есть в архиве.
От себя могу обещать, что продолжу развитие программы в направлении реализации кинематических расчетов моделируемых механизмов (расчет скоростей и ускорений точек механизмов), так что ждите обновлений. Надеюсь они окажутся полезными.

пятница, 21 января 2011 г.

Совместное использование классов GraphingBoard и SimpleGraph

Метод DrawGraph для приложения «Построение графиков производной»

Данный пример покажет возможность непосредственного использования метода drawGraph класса GraphingBoard. Этот метод, с помощью которого вы можете создать массив парных значений (в математических единицах), а затем передать его объекту класса GraphingBoard для непосредственной отрисовки на графике. Т.к. методы класса SimpleGraph позволяют строить лишь несколько предопределенных типов графиков (графики функций в декартовых и полярных координатах, а также параметрических функций), то понимание метода drawGraph класса GraphingBoard даст Вам более гибкие возможности для построения собственных графиков.

Настройка сцены

Настройка сцены заключается в расположении на ней кнопки, слайдера, пяти текстовых полей для ввода текста и одного динамического текстового поля, а также ряда статических полей, поясняющих значение полей ввода (см. рис. 1). Для кнопки назначаем имя btnGraph, для слайдера – slX. Имена пяти текстовых полей: txtFunc, txtXmain, txtXmax, txtYmin, txtYmax. Динамическому текстовому полю, расположенному внизу, задаем имя txtSlope, его ширина должна быть достаточна, чтобы отобразить строку "Slope = -999.99" (уклон касательной).


Добавляем следующий код (здесь я опускаю импорт необходимых классов, смотрите исходники):
public class Main extends Sprite 
{
 // Размеры объекта SimpleGraph 
 private var bSize:Number = 300;
  
 // Длина (в пикселях) касательной
 private var lineLength:Number = 100;
 
 // Переменные для настройки графика 
 private var xticks:Number = 1;
 private var yticks:Number = 1;
 private var xgrids:Number = 1;
 private var ygrids:Number = 1;
 
 // Необходимые текстовые поля
 private var txtFunc:TextField;
 private var txtXmin:TextField;
 private var txtXmax:TextField;
 private var txtYmin:TextField;
 private var txtYmax:TextField;
  
 private var txtSlope:TextField;
 
 private var gderiv:SimpleGraph;
 
 private var slX:HSlider;
 private var btnGraph:PushButton;
 
 // Массивы, хранящие данные о координатах точек 
 // графика функции и ее производной 
 private var arrPx:Array;
 private var arrSlopes:Array;
  
 // Маркеры на графике
 private var shTan:Shape = new Shape();
 private var shPoint:Shape = new Shape(); 
 
Далее, в методе init мы создаем все необходимые текстовые поля, кнопку (этот момент я опускаю) и слайдер, объект SimpleGraph и маркеры, задаем исходное состояние приложения:
gderiv = new SimpleGraph(bSize,bSize);
gderiv.x = 40;
gderiv.y = 50;   
// Чтобы с помощью слайдера можно было отслеживать точку 
// на графике функции, число точек графика должно быть 
// равно диапазону значений слайдера
gderiv.setNumPoints(bSize);
gderiv.board.changeBackColor(0xFFEEEE);
gderiv.board.setCoordsBoxFormat(0xEEEEEE, 0xDDDDDD, 
    0x000000, 12);
gderiv.board.setGrid(xgrids,ygrids);
gderiv.board.setTicks(xticks,yticks,10,10);
addChild(gderiv);
   
slX = new HSlider(this, gderiv.x, gderiv.y + bSize + 10, 
   update);
slX.setSliderParams(0, bSize, 0);
slX.width = bSize;
slX.visible = false;
   
shTan.graphics.lineStyle(1,0x000000);
shTan.graphics.beginFill(0xCC0000,0.8);
shTan.graphics.drawRect(-2,-lineLength/2, 4, lineLength);
shTan.graphics.endFill();
shTan.graphics.beginFill(0xAAAAAA);
shTan.graphics.drawCircle(0,0,4);
shTan.graphics.endFill();
shTan.visible = false;

shPoint.graphics.lineStyle(1,0);
shPoint.graphics.beginFill(0x007700);
shPoint.graphics.drawCircle(0,0,5);
shPoint.graphics.endFill();
shPoint.visible = false;
 
gderiv.addChildToBoard(shTan);
gderiv.addChildToBoard(shPoint);
   
// Исходное состояние графика
txtFunc.text = "sin(x) + cos(x)";
txtXmin.text = "-pi";
txtXmax.text = "pi";
txtYmin.text = "-1.5";
txtYmax.text = "1.5";
drawGraph();
}

Маркер shTan – это вытянутый прямоугольник с точкой регистрации (внутренней точкой отсчета), расположенной в его центре. Чтобы использовать данные об углах наклона кривой, хранящихся в массиве gderiv.getPixData(), для точного позиционирования маркера необходимо, чтобы в исходном состоянии маркер должен быть вертикальным. Итак, маркер shTan будет показывать касательную к графику функции f, введенной пользователем, а маркер shPoint – точку на графике производной функции f.
Для добавления маркеров shTan и shPoint в список отображения объекта gderiv.board мы используем метод addChildtoBoard. Если бы мы использовали метод gderiv.addChild, то к этим маркерам не применилась бы маска объекта gderiv.board, и они оставались бы видимыми даже при выходе за область отрисовки графика:
gderiv.board.addChildToBoard(shTan); 
gderiv.board.addChildToBoard(shPoint); 
 
Функция setup выполняет настройку объекта SimpleGraph (при отсутствии ошибок ввода) и отрисовывает сетку, деления и значения делений.
private function setup():void {
 slX.visible = false;
 shTan.visible = false;
 shPoint.visible = false;

 gderiv.setWindow(txtXmin.text, txtXmax.text, txtYmin.text, 
    txtYmax.text);
 
 if (gderiv.hasError()) return; 

 gderiv.board.drawGrid();
 gderiv.board.drawAxes();
 gderiv.board.drawTicks();
 gderiv.board.addLabels();
} 
 
Функция update является обработчиком события изменения положения слайдера slX, изменяет положения маркеров shPoint и shTan:
private function update(evt:Event):void {
 var i:int = slX.value;
 shTan.x = arrPx[i][0];
 shTan.y = arrPx[i][1];
 shTan.rotation = arrPx[i][2];
  
 shPoint.x = shTan.x;
 shPoint.y = gderiv.board.ytoPix(arrSlopes[i][1]);

 txtSlope.text = "Slope = " + arrSlopes[i][1].toFixed(2);
} 
 
При нажатии на кнопку btnGraph, а также при запуске приложения вызывается функция drawGraph, отвечающая за отрисовку графиков функций:
private function btnGraphClicked(mevt:MouseEvent):void {
 drawGraph();
}
  
private function drawGraph():void {
 var i:int;
 var thisX:Number;
 // Если нет функции для построения графика, 
 // то и делать ничего не нужно
 if (txtFunc.text.length == 0) return;

 setup();
 // при возникновении ошибки график мы не строим
 if (gderiv.hasError())  return;
 // Вычерчиваем график функции, введенной в поле txtFunc 
 gderiv.graphRectangular(txtFunc.text,"x",1,2,0x0000AA);

 if (gderiv.hasError())  return;
   
 gderiv.board.drawGrid();
 gderiv.board.drawAxes();
 gderiv.board.drawTicks();
 gderiv.board.addLabels();
 //  arrPx – это массив, состоящий из массивов по три элемента: 
 // координаты точек и углы наклона касательных в данных точках
 arrPx = gderiv.getPixData(1);
 arrSlopes = new Array(arrPx.length);
 // Заполняем массив точек, по которым будет 
 // строиться график производной функции
 for (i=0; i < arrSlopes.length; i++) {
  thisX = gderiv.board.xfromPix(arrPx[i][0]);
  arrSlopes[i] = [thisX, getSlope(thisX)];
 }
 // Вычерчиваем график производной функции толщиной в 2 
 // пикселя на 2 уровне. 
 // График самой функции вычерчивается на уровне 1 
 gderiv.board.drawGraph(2,2,arrSlopes,0x007700);
   
 shTan.visible = true;
 shPoint.visible = true;
 slX.visible = true;
   
 slX.value = 0;
 // Располагаем маркеры shTan и shPoint 
 // и обновляем отображаемый текст 
 shTan.x = arrPx[0][0];
 shTan.y = arrPx[0][1]; 
 shTan.rotation = arrPx[0][2];
   
 shPoint.x = shTan.x;
 shPoint.y = gderiv.board.ytoPix(arrSlopes[0][1]);

 txtSlope.text = "Slope = " + arrSlopes[0][1].toFixed(2);
}
 
Вспомогательная функция getSlope используется для вычисления приблизительного значения производной в точке xval (используемый здесь прием должен быть знаком Вам из курса школьной алгебры):
private function getSlope(xval:Number):Number {
 var dx:Number = 0.0000001;
 var y1:Number = gderiv.rectangularValueAt(1,xval - dx);
 var y2:Number = gderiv.rectangularValueAt(1,xval + dx);
 return ((y2 - y1)/(2*dx));
} 
 
Использование класса RangeParser для отрисовки графиков в полярных координатах 

В этом примере мы построим простой график функции r = f(θ) в полярных координатах (см. рис. 2). Для этого мы используем метод drawPolar объекта SimpleGraph, но кроме этого нам понадобится кое-что еще: чтобы слайдер мог изменять значения координаты θ в заданных пользователем пределах, мы воспользуемся возможностями класса RangeParser.

Настройка сцены 

На сцене вокруг области отрисовки графиков необходимо расположить множество объектов (текстовые поля для ввода текста, динамические текстовые поля, слайдер). Сама область графиков будет имеет размеры 300х300. Расположение этих объектов не так критично, главное чтобы они не накладывались поверх графика. Пишем код Вы сразу же заметите, что мы импортируем классы из папок parsers и tools, чтобы иметь доступ к классам SimpleGraph, RangeParser и RangeObject. Объекты класса RangeParser будут использоваться для получения из текстовых полей максимальных и минимальных значений угла θ.
import flashandmath.as3.parsers.*; 
import flashandmath.as3.tools.*; 
 
В этом примере мы используем бОльший диапазон значений для слайдера, а, следовательно, и угла θ, что должно сделать график функции более гладким. Т.к. мы снова используем положение слайдера в качестве индекса в массиве отрисовываемых точек, то количество точек графика тоже должно равняться диапазону значений слайдера.
private var bSize:Number = 320;
var arrPx:Array; 
var tmin:Number; 
var tmax:Number;

// Фрагмент метода init
private function init(e:Event = null):void 
{   
 …

 // Добавляем кнопки
 btnGraph = new PushButton(this, 456, 398, "ЧЕРТИТЬ", 
      btnGraphClicked);
 btnGraph.width = 82;
 btnGraph.height = 24;
   
 btnReset = new PushButton(this, 547, 398, "СБРОС", clearGraph);
 btnReset.width = 82;
 btnReset.height = 24;
   
 // Добавляем слайдер
 slT = new HSlider(this, 50, 510, update);
 slT.setSliderParams(0, 2*bSize, 0);
 slT.width = 300;
   
 gpolar = new SimpleGraph(bSize, bSize);
 gpolar.x = 40;
 gpolar.y = 100;
 // Количество точек графика, по которым он строится,
 // должно быть равным диапазону значений слайдера
 gpolar.setNumPoints(slT.maximum);
 addChild(gpolar);
 
 // Создаем маркер точки на графике
 shTrace = new Shape();
 shTrace.graphics.lineStyle(1,0x000000);
 shTrace.graphics.drawCircle(0,0,5);
 shTrace.graphics.moveTo(-5,0);
 shTrace.graphics.lineTo(5,0);
 shTrace.graphics.moveTo(0,-5);
 shTrace.graphics.lineTo(0, 5);
 // Добавляем маркер в список отображения
 gpolar.addChildToBoard(shTrace);
   
 shTrace.visible = false;
 slT.visible = false;
 
 … 
 drawGraph();
} 
 
В функции update мы преобразовываем положение слайдера в значение угла θ, лежащее в пределах между tmin и tmax, и используем его для вычисления значения функции r. Также мы располагаем маркер на графике и используем его координаты для отображения математических координат точки. Объект класса SimpleGraph сам выполняет преобразование полярных координат в прямоугольные для построения массива координат точек arrPix.
private function update(evt:Event):void {
 var i:int = slT.value;
 var curt:Number = tmin + i*(tmax-tmin)/
    (slT.maximum - slT.minimum);
 var curr:Number = gpolar.polarValueAt(1, curt);
   
 shTrace.x = arrPix[i][0];
 shTrace.y = arrPix[i][1];

 var curx:Number = gpolar.board.xfromPix(shTrace.x);
 var cury:Number = gpolar.board.yfromPix(shTrace.y);
    
 txtX.text = curx.toFixed(2);
 txtY.text = cury.toFixed(2);
 txtT.text = curt.toFixed(2);
 txtR.text = curr.toFixed(2);
} 
 
Всю основную работу выполняет фунция drawGraph, вызываемая при щелчке по кнопке btnGraph. Создав данную функцию, мы можем вызвать ее из любого места приложения.
private function btnGraphClicked(mevt:MouseEvent):void {
 drawGraph();
} 
 
Для получения значений границ диапазона изменения координат для отрисовки графика мы используем возможности класса RangeParser. У класса RangeParser есть 2 метода: parseRangeTwo и parseRangeFour, возвращающих объект класса RangeObject . А у объектов класса RangeObject есть 3 свойства: Values (значения), errorStatus (состояние) и errorMes (сообщение об ошибке). Названия последних двух говорят сами за себя. Свойство Values является массивом, содержащим 2 (4) численных значения, (при успешном итоге их анализа).
private function drawGraph():void {
 var stR:String;
 var stMin:String;
 var stMax:String;
 var curx:Number;
 var cury:Number;
 var curt:Number;
 var curr:Number;
 // Создаем объект класса RangeParser для получения 
 // значений границ диапазона изменения координаты θ 
 var rp:RangeParser = new RangeParser();
 var ro:RangeObject;
 // Просто скрываем графические элементы до тех пор, 
 // пока не убедимся в отсутствии ошибок 
 shTrace.visible = false;
 slT.visible = false;
 txtX.text = "";
 txtY.text = "";
 txtT.text = "";
 txtR.text = "";
   
 gpolar.setWindow(txtXmin.text, txtXmax.text, 
    txtYmin.text, txtYmax.text);
   
 if (gpolar.hasError())  return; }
 setupBoard();
   
 stR = txtFunR.text;
 stMin = txtTmin.text;
 stMax = txtTmax.text;
   
 gpolar.graphPolar(stR, "theta", stMin, stMax, 1, 2, 0x0000AA);
   
 if (gpolar.hasError())  return; 
   
 // Используем объект класса RangeParser для получения 
 // численных значенией границ изменения угловой координаты, 
 // введенных в текстовые поля txtTmin и txtTmax.
 ro = rp.parseRangeTwo(stMin, stMax);
 if (ro.errorStatus == 1) return; 
 // Получаем численные значения границ графика функции
 tmin = rp.parseRangeTwo(stMin, stMax).Values[0];
 tmax = rp.parseRangeTwo(stMin, stMax).Values[1];
 // Получаем массив координат точек графика, 
 // который мы можем использовать в дальнейшем 
 arrPix = gpolar.getPixData(1);
 // Если выполнение функции не прекратилось до сих пор, 
 // то ошибок пользовательского ввода не возникло, 
 // и мы можем сделать видимыми скрытые ранее 
 // графические элементы и настроить начальное положение маркера
 
 shTrace.visible = true;
 slT.visible = true;
 slT.value = 0;
 shTrace.x = arrPix[0][0];
 shTrace.y = arrPix[0][1];
   
 curx = gpolar.board.xfromPix(shTrace.x);
 cury = gpolar.board.yfromPix(shTrace.y);
 curt = tmin;
 curr = gpolar.polarValueAt(1, curt);
   
 txtX.text = curx.toFixed(2);
 txtY.text = cury.toFixed(2);
 txtT.text = curt.toFixed(2);
 txtR.text = curr.toFixed(2);
} 
 
Щелчок по кпонке СБРОС должен очищать значения x, y, r и θ, стирать график функции и скрывать слайдер и маркер:
private function clearGraph(e:MouseEvent):void
{
 shTrace.visible = false;
 slT.visible = false;
 txtX.text = "";
 txtY.text = "";
 txtT.text = "";
 txtR.text = "";
 gpolar.removeGraph(1);
 gpolar.setWindow(txtXmin.text, txtXmax.text, 
    txtYmin.text, txtYmax.text);
} 
 
Функция setupBoard нужна для перерисовки делений и осей после вызова функции setWindow:
private function setupBoard():void {
 gpolar.board.changeBackColor(0xFFFFEE);
 gpolar.board.setCoordsBoxFormat(0xEEEEEE, 0xDDDDDD, 
      0x000000, 12);
 gpolar.board.drawAxes(); 
 gpolar.board.setTicks(1,1,10,10);
 gpolar.board.drawTicks();
} 
 
Этот пост – последний из серии переводов 12 главы книги "Flash and Math Applets: Learn by Example". В качестве бонуса кроме исходников приведенных здесь примеров прикладываю дополнительный пример, позволяющий строить графики параметрических функций. Скачать их можно здесь. Если Вас интересуют и другие возможности классов пакета flashandmath и знания английского позволяют, то в качестве дополнительных ресурсов очень рекомендую уроки с сайта www.flashandmath.com разделы Intermediate и Advanced tutorials (откуда, собственно, и были взяты все материалы).

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

Справка по классам GraphingBoard и SimpleGraph

Класс flashandmath.as3.parsers.RangeObject

Описание

RangeObject – это вспомогательный по отношению к RangeParser класс. Объекты данного класса представляют собой особый тип данных, возвращаемый методами parseRangeTwo и parseRangeFour класса RangeParser. Этот тип данных представляет собой объект, содержащий три свойства, перечисленных ниже, и включает в себя результаты анализа введенных пользователем диапазонов изменения двух переменных (например, X и Y), или одной переменной (скажем, параметра t). В большинстве приложений могут присутствовать поля для ввода диапазонов значений переменных, возможности данного класса позволяют вводить в эти поля, как простые числа, так и алгебраические выражения, содержащие число пи (pi/4, 2*pi и т.д.). Эти данные пользовательского ввода должны быть проанализированы и проверены на валидность.

Конструктор

Конструктор вызывается с помощью ключевого слова new без аргументов:
new RangeObject();
Обычно Вам не придется сталкиваться с необходимостью использования конструктора напрямую, поскольку полезными для применения объектами RangeObject могут быть только возвращаемые методами parseRangeTwo или parseRangeFour класса RangeParser.


Общедоступных методов у этого класса нет

Общедоступные свойства

У объектов класса RangeObject есть три таких свойства.
instance.Values:Array
Когда экземпляр класса RangeObject возвращается одним из методов класса RangeParser, то данный массив содержит в себе четыре значения границ диапазонов изменения переменных (при анализе диапазонов значений для двух переменных) или два значения границ диапазона (при анализе диапазона значений одной переменной, т.е. для каждой переменной задается минимальное и максимальное значения, в пределах которых можно будет построить графики функций).

instance.errorStatus:Number
Данное свойство равно 1 при обнаружении ошибки и 0 – в противном случае (Опять же, значение этого свойства устанавливается методами класса RangeParser).

instance.errorMes:String
Данное свойство содержит описание ошибки, позволяющее найти ее в пользовательском вводе.

Класс flashandmath.as3.parsers.RangeParser

Описание

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

Конструктор

Конструктор вызывается с помощью ключевого слова new и не требует аргументов:
new RangeParser();
Например: var procRange:RangeParser = new RangeParser();

Общедоступные методы

У объектов класса RangeParser есть два таких метода.

instance.parseRangeTwo(par1:String, par2:String):RangeObject
Данный метод принимает в качестве аргументов две строковых величины (обычно введенные пользователем границы диапазона значений какого-либо параметра, например t) и возвращает экземпляр класса RangeObject.
Если введенные пользователем значения действительно являются численными величинами (или выражениями) и численное значение первого параметра меньше второго, то свойства возвращаемого экземпляра класса RangeObject равны: Values содержит значения границ диапазона; errorStatus = 0; errorMes = "".
При обнаружении ошибки свойства возвращаемого экземпляра класса RangeObject равны: errorStatus = 1; Values = []; а errorMes – содержит сообщение, указывающее на место возникновения ошибки.

instance.parseRangeFour(par1:String, par2:String, par3:String, par4:String):RangeObject
Данный метод аналогичен предыдущему, но выполняет анализ уже для границ диапазонов двух переменных, при этом должны соблюдаться условия: численное значение первого параметра должно быть меньше второго, а численное значение третьего – меньше четвертого.


Общедоступных свойств у этого класса нет

Класс flashandmath.as3.boards.GraphingBoard

Описание

GraphingBoard – это основной визуальный класс для создания настраиваемых двумерных графиков: графиков функций одной переменной, графиков параметрических функций и т.д. Экземпляр класса GraphingBoard создает прямоугольную область для отрисовки графиков с координатными осями и графиками функций, параметрических кривых. Любой экземпляр класса GraphingBoard также содержит текстовые поля для отображения: ошибок; координат мыши на графике. Имеется возможность "рисования" мышью на графике.
Верстка, цвета, и размеры всех элементов экземпляра класса GraphingBoard легко настраиваются с помощью методов класса.
Экземпляр класса GraphingBoard устанавливает диапазоны значений координат X и Y (как правило, на основе введенных пользователем данных), и предоставляет методы для перевода пиксельных координат точки в его функциональные координаты и наоборот.

Класс GraphingBoard наследует от класса Sprite. Таким образом, он обладает теми же свойствами и методами, что и класс Sprite. В частности, вы можете контролировать положение своего объекта GraphingBoard следующим образом:
instance.x = 200;
instance.y = 100;

Конструктор

Конструктор вызывается с помощью ключевого слова new и принимает два численных параметра: ширину и высоту (в пикселях) области для вычерчивания графиков:
new GraphingBoard(w:Number, h:Number);
Все остальные атрибуты графика настраиваются с помощью методов класса.

Методы для управления внешним видом области отрисовки графиков

Цвет фона графической области задается методом:
instance.changeBackColor(col:Number):void
По умолчанию цвет фона – белый.

Цвет и толщина рамки вокруг области отрисовки графиков задаются методом:
instance.changeBorderColorAndThick(col:Number, t:Number):void
Здесь первый аргумент – цвет линий, второй – толщина.
Для вновь создаваемых объектов класса цвет рамки – черный

Цвет и толщина координатных осей можно установить методом:
instance.setAxesColorAndThick(col:Number, t:Number):void
Первый аргумент – цвет осей, второй – толщина. Устанавливая значение толщины равным 0, мы получим оси толщиной в 1 пиксель, не изменяющие ее при изменении масштаба.
Значения по умолчанию: черный, 0 пикс.

Метод changeBackAlpha позволяет создавать графики без фона, устанавливая значение "прозрачности фона" равной 0:
instance.changeBackAlpha(alpha:Number):void

Следующие методы задают параметры сетки, делений и обозначений:
instance.setGridColor(col:Number):void – цвет сетки
instance.setGrid(xsize:Number, ysize:Number):void – шаг сетки по осям X и Y
instance.drawGrid():void – отрисовка сетки
instance.setTicks(xsize:Number,ysize:Number,xheight:Number,ywidth:Number):void – задание цены делений (xsize и ysize) и высоты линий делений (xheight, ywidth)
instance.drawTicks():void – отрисовка делений
instance.setLabelFormat(f:String,n:Number,c:uint):void – задание стиля надписей (f – шрифт, n – размер, c – цвет)
instance.addLabels():void – отрисовка значений делений
instance.clearLabels():void – соответственно, убирает значения делений

Методы для рисования на области графиков с помощью мыши

instance.enableUserDraw(col:Number, t:Number):void – данный метод включает возможности рисования пользователем с помощью мыши. В качестве аргументов принимает значения цвета и толщины линий пользовательской графики.
Значения по умолчанию: данная возможность включена, цвет линий – красный, толщина – 0.

Если же Вы хотите отключить такую возможность, то воспользуйтесь методом:
instance.disableUserDraw():void

Методы для управления отображением ошибок

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

instance.setErrorBoxSizeAndPos(w:Number,h:Number,xpos:Number,ypos:Number):void
Параметры данного метода задают ширину, высоту и положение текстового поля (положение задается относительно положения объекта GraphingBoard).
По-умолчанию данное текстовое поле расположено в верней части объекта GraphingBoard.

Внешний вид текста, отображаемого в этом поле можно настроить с помощью метода:
instance.setErrorBoxFormat(col1:Number, col2:Number, col3:Number,s:Number): void
где col1 – цвет фона текстового поля, col2 – цвет рамки вокруг поля, col3 – цвет текста, s – размер шрифта.
По-умолчанию у этого текстового поля белые фон и рамка, шрифт – черный, 12пт.
Примечание: текстовое поле для отображения сообщений об ошибках являет общедоступным (public) свойством объектов класса GraphingBoard:
instance.ErrorBox
Поэтому, управлять его свойствами возможно напрямую. Однако лучшей практикой является обращение к нему через методы класса GraphingBoard.
Текстовое поле ErrorBox сделано общедоступным в целях облегчения управления его видимостью и текстом, отображаемым в нем (это текстовое поле становится видимым, когда пользователь допускают какую-либо ошибку, а его текст определяется типом ошибки).
Заметьте, что в начальном состоянии данное текстовое поле невидимо.

Методы для отображения координат мыши на графике

Также у объектов класса GraphingBoard есть текстовое поле, показывающее координаты мыши на нем. С помощью следующих методов Вы можете контролировать его положение и внешний вид.
instance.setCoordsBoxSizeAndPos(w:Number,h:Number, xpos:Number,ypos:Number): void
параметры этого метода задают размеры и положение текстового поля относительно положения объекта GraphingBoard.
По-умолчанию текстовое поле расположено в нижнем левом углу объекта.

Цвет фона и рамки вокруг текстового поля, а также цвет и размер шрифта текста устанавливаются методом:
instance.setCoordsBoxFormat(colo1:Number,colo2:Number,colo3:Number,s:Number): void
Параметры этого метода полностью аналогичны параметрам метода setErrorBoxFormat (см. выше). По-умолчанию стиль данного текстового поля такой же, как и у поля ErrorBox.

Отображение координат мыши на графике также можно включать и выключать:
disableCoordsDisp():void
enableCoordsDisp(h:String,v:String):void, где h и v – "имена" осей, отображаемые в текстовом поле.
По-умолчанию координаты мыши показываются.

Методы для трассировки точек

Класс GraphingBoard предоставляет несколько полезных методов для отслеживания положения точек. Класс предоставляет два стиля для отображения курсора: cross (крест) и arrow (стрелка). Стиль устанавливается методом:
instance.setTraceStyle(s:String):void
где параметр s должен принимать одно из двух значений cross или arrow.
Стиль по умолчанию: cross.
Для каждого из стилей есть возможности управления размером и цветом курсора. Далее описаны соответствующие методы.

Размеры и толщина линий курсора типа cross задаются методом:
instance.setCrossSizeAndThick(s:Number,t:Number):void
По-умолчанию размеры перекрестья – 6 пикс, толщина линий – 1 пикс.

Цвет перекрестья задается с помощью метода (по умолчанию черный):
instance.setCrossColor(col:Number):void

Управление положением курсора осуществляется методом
instance.setCrossPos(xpos:Number, ypos:Number):void
Положение задается в пикселях относительно левого верхнего края объекта GraphingBoard. По-умолчанию курсор находится в точке (0; 0).

Видимость перекрестья задается с помощью (по умолчанию невидимо):
instance.crossVisible(b:Boolean):void

Получение размеров перекрестья:
instance.getCrossSize():Number

Для курсора типа arrow (стрелка) имеются аналогичные методы.

Размеры стрелки:
instance.setArrowSize (s:Number):void

Цвет стрелки (по умолчанию черная):
instance.setArrowColor(col:Number):void

Положение (в пикселях относительно верхнего левого края объекта GraphingBoard) и поворот (отсчитываемый против хода часовой стрелки, в градусах) задаются методом:
instance.setArrowPos(xpos:Number, ypos:Number, rot:Number):void
По-умолчанию стрелка находится в верхнем левом углу и указывает вверх.

Видимость стрелки управляется методом (по умолчанию невидима):
instance.arrowVisible(b:Boolean):void

Чтобы узнать текущие размеры стрелки используйте:
instance.getArrowSize():Number


Методы для вычерчивания графиков

Для каждого экземпляра класса GraphingBoard количество одновременно отрисовываемых графиков задается методом (по умолчанию 3):
instance.setMaxNumGraphs(a:int):void

Перед тем, как Вы сможете приступить к отрисовке графиков, необходимо задать границы изменения координат x и y:
instance.setVarsRanges(xmin:Number, xmax:Number, ymin:Number, ymax:Number): void
Значения по умолчанию: отсутствуют.

Как только Вы задали диапазоны координат, можно нарисовать координатные оси:
instance.drawAxes(): void

Следующие методы позволяют рисовать точки, точки разрыва и вертикальные асимптоты. Заметьте, что цвет всех этих элементов задается первым методом:
instance.setCircleColor(col:Number): void
instance.addOpenPoint(p:Point):void – добавление точки разрыва ("пустой" точки)
instance.addClosedPoint(p:Point):void – добавление точки
instance.addVertAsymptote(nx:Number):void – добавление вертикальной ассимптоты
instance.drawPoints():void – отрисовка добавленных точек и ассимптот
instance.clearPoints():void – удаление добавленных точек как из графика, так и из памяти

Графики функций и кривые отрисовываются с помощью метода:
instance.drawGraph(num:int, thick:Number, aVals:Array, col:Number): Array
Первый параметр метода – номер отрисовываемого графика (не должен превышать максимального числа отрисовываемых графиков, заданного методом setMaxNumGraphs). Этот параметр также задает и "уровень" графика (график с большим значением "уровня" будет отображаться поверх графика с меньшим). Второй параметр задает толщину линий графика. Третий параметр должен быть массивом, состоящим из массивов пар значений координат точек. Координаты точек в этих массивах должны быть заданы в единицах измерения функций (графики которых строятся), объект GraphingBoard сам позаботится об их переводе в пиксели. Учтите, что данный метод просто берет координаты точек и последовательно соединяет их отрезками. Последний параметр задает цвет линии графика.
Метод drawGraph возвращает массив, содержащий значения координат точек на графике, но уже в пикселях.

Методы для очистки области отрисовки графиков

Для удаления графиков используйте метод:
instance.cleanBoard():void
Этот метод стирает все графики и оси, сбрасывает значения диапазонов изменения координат (после этой операции их снова надо будет задавать методом setVarsRanges). Однако данный метод не стирает графику нарисованную пользователем с помощью мыши, для этих целей есть метод:
instance.eraseUserDraw():void


Остальные методы класса GraphingBoard

Вот несколько дополнительных методов, которые могут оказаться полезными.

instance.getMaxNumGraphs():int – возвращает установленное значение максимального количества графиков

Для определения размеров графической области для вычерчивания графиков используйте методы:
instance.getBoardWidth():Number
instance.getBoardHeight():Number

Получить значения границ диапазонов координат x и y графика (если они были установлены ранее) позволяет метод:
instance.getVarsRanges(): Array

Для установки и получения значения количества точек, по которым строятся графики используйте методы:
instance.setNumPoints(np:Number):void
instance.getNumPoints():Number
Количество точек задает количество отрезков, из которых и получаются кривые.

Следующие четыре метода позволяют Вам преобразовывать координаты точек из единиц измерения, принятых в вычерчиваемых функциях, в пиксели и наоборот:
instance.xtoPix(a:Number): Number
instance.ytoPix(a:Number): Number
instance.xfromPix(a:Number): Number
instance.yfromPix(a:Number): Number

Вместо последних двух можно использовать следующие методы (эти методы остались для обратной совместимости, хотя их код полностью идентичен):
instance.xtoFun(a:Number): Number
instance.ytoFun(a:Number): Number

Заметим, что данные методы работают только при заданных значениях диапазонов изменения координат, в противном случае они возвращают NaN.

Парочка методов для проверки значений аргументов:

instance.function isLegal(a:*):Boolean – данный метод возвращает true, если аргумент является численным типом данных конечной величины, в противном случае возвращается false.
Метод instance.isDrawable(a:*):Boolean выполняет те же проверки, но добавляет условие того, что абсолютное значение аргумента не должно превышать "5000". Данное условие добавлено из-за возможности появления неожиданных результатов при попытке "нарисовать" что-либо за пределами этой величины.

Наконец, если Вам необходимо удалить экземпляр класса GraphingBoard, используйте метод
instance.destroy():void
Этот метод удаляет все обработчики событий, стирает всю графику, и устанавливает значения всех объектов Sprite, созданных им, в null.

Общедоступные свойства

Единственным таким свойством (за исключением всех унаследованных от класса Sprite) является динамическое текстовое поле для отображения ошибок: instance.ErrorBox.
Как упоминалось ранее управлять его свойствами можно как с помощью методов класса GraphingBoard, так и напрямую методами класса TextField, например:
instance.ErrorBox.visible = true;
instance.ErrorBox.text = “Error in f1(x). ” + compObj1.errorMes;

Класс flashandmath.as3.tools.SimpleGraph

Конструктор

new SimpleGraph(w, h)
Конструктор создает новый экземпляр класса GraphingBoard шириной w и высотой h, для доступа к которому используется геттер instance.board.

Общедоступные методы

instance.setWindow(sXmn:String, sXmx:String, sYmn:String, sYmx:String):void
Этот метод проверяет входные параметры, и при отсутствии ошибок создает пустое поле для графиков, задавая ему диапазоны изменения координат.

В каждом из следующих методов аргумент expr – это строковая величина, представляющая собой выражение для функции одной переменной с именем stVar; график данной функции будет отрисовываться на "уровне" num линиями с цветом color и толщиной thickness. Для графиков в полярных координатах или параметрических функций необходимо дополнительно задавать минимальное и максимальное значения параметров:
instance.graphRectangular(expr:String, stVar:String, num:int, thickness:Number, color:Number):void
instance.graphPolar(expr:String, stVar:String, sTmin:String, sTmax:String, num:int, thickness:Number, color:Number):void
instance.graphParametric(expr1:String, expr2:String, stVar:String, sTmin:String, sTmax:String, num:int, thickness:Number, color:Number):void

Следующий метод сообщает о наличии ошибок, возникших при анализе диапазонов изменения переменных и самих выражений:
instance.hasError():Boolean

Создав экземпляр класса SimpleGraph, мы можем взаимодействовать следующими способами:

instance.rectangularValueAt(num:int, xval:Number):Number – возвращает значение выражения (график которого отображается на “
"уровне" num) при значении переменной равном xval. Для функций в декартовых координатах.

instance.polarValueAt(num:int, theta:Number):Number – аналогичный метод для функций в полярных координатах.

instance.parametricValueAt(num:int, tval:Number):Array – тоже аналогично, но возвращает не число, а массив, содержащий два элемента [x(val), y(tval)].

Во многих случаях не возникает необходимости рассчитывать значения выражений (графики которых строятся) отдельно. Но для доступа к значениям координат точек (в пикселях), по которым строится график, существует метод:
instance.getPixData(num:int):Array, возвращающий массив, состоящий из массивов по три элемента в виде [xcrd, ycrd, rotat], где первые два элемента – координаты точки, а третий – угол наклона касательной к графику функции в данной точке (этот угол можно использовать, например, при необходимости показать касательную в данной точке).

Получить остальные важные данные об объекте SimpleGraph и функциях, графики которых были построены, можно следующими методами:
instance.getCompiledObject(num:int):Array
instance.getVariable(num:int):String – возвращает имя переменной функции
instance.getNumPoints():int – возвращает количество точек, по которым был построен график
instance.setNumPoints(np:int):void – задает количество точек, по которым будут строится графики. Изменяя данное значении, можно регулировать точность построения графика.

Метод
instance.addChildToBoard(obj:DisplayObject):void
добавляет графический объект obj в область отрисовки графиков, таким образом к нему применяется маска объекта board (экземпляра класса GraphingBoard).

Названия следующих методов говорят сами за себя:
instance.removeGraph(num:Number):void
instance.destroy():void


Общедоступные свойства

Единственным таким свойством является свойство board:GraphingBoard, поэтому для настройки внешнего вида области графиков можно использовать методы класса GraphingBoard, обращаясь к данному свойству.

понедельник, 10 января 2011 г.

Введение в класс SimpleGraph

Класс SimpleGraph: первый пример

Приложение, созданное в этом примере, будет чертить графики двух "вшитых" в код функций, отслеживать положения точек на них, а также показывать расстояние между ними. Использование данного приложения должно помочь в нахождении самого большого расстояния между двумя кривыми - классической задаче из задачника (в реальной жизни данная задача решается с помощью производных).
Цель данного примера – показать возможности использования класса SimpleGraph, позволяющего создавать более сложные приложения.

Простейший пример использования класса SimpleGraph

Самое важное свойство класса SimpleGraph – это свойство board, являющееся экземпляром другого класса – GraphingBoard. После обсуждения данного примера мы рассмотрим возможности использования методов класса GraphingBoard для усовершенствования внешнего вида графиков.

Настройки сцены

В этом приложении нам понадобится слайдер – slX и динамическое текстовое поле txtDistance.

Пишем код

Прежде всего, необходимо импортировать классы пакета flashandmath.as3.tools (SimpleGraph входит в состав данного пакета). Помните, что папка flashandmath должна быть в той же папке, что и исходный файл проекта. Ну и кроме этого импортируем дополнительные самого флеша

package 
{
import flashandmath.as3.tools.*;

import com.bit101.components.HSlider;
import flash.display.Shape;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.text.TextField;
import flash.text.TextFieldType;
import flash.text.TextFormat;
import flash.text.TextFormatAlign;
import flash.ui.Keyboard;
import teormech.utils.TextUtils;

public class Main extends Sprite 
{

Объявляем необходимые переменные класса:

// Текстовое поле для отображения расстояния между кривыми
private var txtDistance:TextField;

// Горизонтальный слайдер для обеспечения возможности 
// взаимодействия с графиками функций 
private var slX:HSlider;

// Размеры области графика и сам объект класса SimpleGraph 
// для отрисовки графиков
private var bSize:Number = 300;
private var gsimp:SimpleGraph;

// Массивы для хранения координат точек графиков функций
private var arrPx1:Array;
private var arrPx2:Array;

// Маркеры для обозначения текущих точек на графике 
// и расстояния между ними
private var sh1:Shape;
private var sh2:Shape;
private var shLine:Shape;

Рассмотрим упрощенно принципы построения графиков внутри класса SimpleGraph. Т.к. один объект SimpleGraph может отображать несколько графиков различных функций, то для доступа к ним введено понятие "уровня" (если угодно, "глубины") графика i. Для каждого графика создается и сохраняется массив значений координат точек, доступ к которому можно получить, вызвав метод gsimp.getPixData(i) (где i – "уровень" графика). Размер этого массива определяется свойством numPoints объекта SimpleGraph. Поэтому, если у нашего слайдера диапазон значений будет изменяться от 0 до numPoints, то мы сможем использовать значения положения слайдера, как индекс для точки графика, и тем самым обеспечим их прямую связь.

// Фрагмент метода init()

var tempFormat:TextFormat = 
new TextFormat("Arial", "14", "0x000000", true);
tempFormat.align = TextFormatAlign.CENTER;

txtDistance = 
TextUtils.createTextField("", 415, 280, tempFormat, 120, 20);
addChild(txtDistance);

// Создаем экземпляр класса SimpleGraph с областью 
// для отрисовки графиков 300 на 300 пикселей  
gsimp = new SimpleGraph(bSize,bSize);
gsimp.x = 25;
gsimp.y = 10;
addChild(gsimp);

// Устанавливаем количество точек, по которым строится график, 
// равным диапазону значений нашего слайдера (см. далее)
gsimp.setNumPoints(2*bSize);

// Устанавливаем пределы изменения координат на графике
gsimp.setWindow("-pi","sqrt(10)","-1.5","1.5");

// Строим графики двух функций 
// (один синего цвета, второй  - зеленого)
// Заметьте, что первому графику мы задаем значение глубины 
// равным 1, а второму 2. 
// Зная эти значения, мы имеем доступ к этим графикам
gsimp.graphRectangular("sin(x)","x",1,2,0x0000AA);
gsimp.graphRectangular("cos(x)", "x", 2, 2, 0x00AA00);

// Здесь видно, как можно получить данные о координатах 
// точек графика, передавая методу getPixData в качестве 
// аргумента значения глубины графика 
arrPx1 = gsimp.getPixData(1);
arrPx2 = gsimp.getPixData(2);

// Создаем маркеры точек на графиках 
// и строим соединяющую их линию    
sh1 = new Shape();
sh1.graphics.lineStyle(1,0);
sh1.graphics.beginFill(0x0000AA);
sh1.graphics.drawCircle(0,0,6);
sh1.graphics.endFill();

// Здесь видно, как можно получить данные 
// о координатах точек графика.
// Они хранятся в виде вложенных массивов, 
// первыми элементами которых являются значения 
// координаты "x", а вторыми – значения "y" (в пикселях)
sh1.x = arrPx1[0][0];
sh1.y = arrPx1[0][1];

sh2 = new Shape();
sh2.graphics.lineStyle(1,0);
sh2.graphics.beginFill(0x00AA00);
sh2.graphics.drawCircle(0,0,6);
sh2.graphics.endFill();
sh2.x = arrPx2[0][0];
sh2.y = arrPx2[0][1];

shLine = new Shape();
shLine.graphics.lineStyle(2,0x000000);
shLine.graphics.moveTo(sh1.x,sh1.y);
shLine.graphics.lineTo(sh2.x,sh2.y);

// Создаем слайдер, располагаем его под графиком функции, 
// устанавливаем его ширину равной ширине графика, а также 
// задаем ему нужный нам диапазон значений [0; 2 * bSize]
slX = new HSlider(this, 25, 330, update);
slX.setSliderParams(0, 2 * bSize, 0);
slX.width = bSize;

Т.к. свойство board объекта SimpleGraph не является общедоступным, то мы не можем добавлять на него объекты отображения напрямую. Для этого нужно использовать метод addChildToBoard. Преимущество данного способа добавления пользовательской графики в том, что у объекта board есть маска и при использовании метода addChildToBoard она автоматически применяется к ней.

// Функция update вызывается при перетаскивании слайдера. 
// Используя его положение в качестве индекса в массиве 
// значений координат точек, мы можем динамически изменять 
// положения маркеров, отрисовывать линию соединяющую их, 
// и обновлять значение расстояния между точками графиков 
private function update(evt:Event):void 
{
var i:int = slX.value;
sh1.x = arrPx1[i][0];
sh1.y = arrPx1[i][1];
sh2.x = arrPx2[i][0];
sh2.y = arrPx2[i][1];

shLine.graphics.clear();
shLine.graphics.lineStyle(2,0x000000);
shLine.graphics.moveTo(sh1.x,sh1.y);
shLine.graphics.lineTo(sh2.x,sh2.y);
txtDistance.text = getDistance();
}

Функция getDistance используется из-за того, что значение вертикального расстояния между точками должно выводится не в пикселях, а в математических единицах. Для этого используется метод yfromPix (у него есть "собрат" – метод xfromPix, преобразующий значения горизонтальной координаты).

private function getDistance():String {
var dy:Number = Math.abs(gsimp.board.yfromPix(sh1.y) - 
    gsimp.board.yfromPix(sh2.y));
return ("Distance = " + dy.toFixed(2));
}

Теперь рассмотрим некоторые методы класса GraphingBoard.

Настройка внешнего вида графиков с использованием класса GraphingBoard

Как мы видели у объектов класса SimpleGraph есть свойство board, являющееся объектом класса GraphingBoard. Объекты этого класса содержат прямоугольный фон и маску, а также обладают множеством свойств и методов для управления внешним видом графиков.

Немного усовершенствованный пример (внешний вид графика настраивается с помощью методов класса GraphingObject

Пример «Вертикальное расстояние». Второй подход

Созданному нами приложению «Вертикальное расстояние» в том виде, в каком оно сейчас находится, явно не хватает деталей: мы не видим сетки, меток, обозначений осей и не можем оценить расстояния между точками на глаз. Покажем как все это сделать.
Откроем файл приложения, созданного в предыдущем примере, и добавим следующий код в метод init() (сразу за строкой var arrPx2:Array = gsimp.getPixData(2);):

gsimp.board.changeBackColor(0xFFEEEE); 
gsimp.board.changeBorderColorAndThick(0xDDDDDD,4); 
gsimp.board.setAxesColorAndThick(0x777777,2); 
gsimp.board.setCoordsBoxFormat( 0xEEEEEE, 0xDDDDDD, 
                                     0x000000, 12);
gsimp.board.drawAxes(); 
gsimp.board.setGrid(0.5,0.25); 
gsimp.board.drawGrid(); 
gsimp.board.setTicks(1,1,10,10); 
gsimp.board.drawTicks(); 
gsimp.board.addLabels();

Имена методов класса GraphingBoard говорят сами за себя.

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

среда, 5 января 2011 г.

Справка по классам MathParser и CompiledObject

В качестве дополнения к предыдущему посту привожу здесь перевод справки по классам MathParser и CompiledObject

Класс flashandmath.as3.parsers.CompiledObject

Описание: Данный класс является вспомогательным для класса MathParser. Объекты этого класса хранят в себе результаты синтаксического анализа выражения (осуществляемого с помощью метода doCompile объекта класса MathParser) в форме, удобной для дальнейших вычислений. Объекты данного класса содержат три свойства, перечисленных ниже.

Конструктор класса

Конструктор класса вызывается стандартным способом и не принимает никаких аргументов: new CompiledObject();
Вам не придется вызывать конструктор класса напрямую, т.к. экземпляры данного класса, которые можно хоть как-то использовать, возвращаются методом doCompile объектами класса MathParser.

Общедоступных методов у этого класса нет.

Свойства

У объектов класса CompiledObject есть три общедоступных свойства:
instance.PolishArray:Array – данный массив представляет собой математическую формулу анализируемого выражения в Польской нотации.
instance.errorStatus:Number – данное свойство при обнаружении ошибок в анализируемом выражении содержит 1, а при отсутствии ошибок – 0.
instance.errorMes:String – данная строка содержит сообщение для пользователя, говорящее о том, где в пользовательском вводе обнаружена ошибка.

Класс flashandmath.as3.parsers.MathParser

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

Конструктор класса

Конструктор класса вызывается стандартным способом (с помощью ключевого слова new):
new MathParser(parameter1:Array)
Конструктор принимает один аргумент, являющийся массивом, элементами которого являются строки (представляющие собой имена переменных, входящих в анализируемое выражение). Например:
var procFun:MathParser = new Parser(["x", "y"]);
Объект procFun будет "понимать", что выражение содержит переменные x и y. Объекты класса MathParser также распознают константы e (основание натурального логарифма) и pi (число пи), поэтому не давайте переменным такие имена.
Заметьте, что имена переменных должны быть строковыми величинами:
var procFun:MathParser = new Parser(["x"]);
а не:
var procFun:MathParser = new Parser([x]);
Важно помнить порядок имен переменных в массиве, передаваемом в качестве аргумента конструктора, т.к. методы для расчета значений выражений будут ожидать, что Вы будете передавать значения переменных в том же порядке.

Общедоступные методы

У объектов класса MathParser есть три таких метода:
instance.doCompile(parameter1:String):CompiledObject – этот метод принимает в качестве аргумента строку (обычно это математическая формула, введенная пользователем) и возвращает экземпляр класса CompiledObject. При отсутствии синтаксических ошибок свойства возвращаемого объекта класса CompiledObject, содержат:
- PolishArray – формулу, записанную в Польской нотации;
- errorStatus = 0;
- errorMes = "".
Если же ошибка был найдена, то errorStatus = 1, а свойство errorMes содержит сообщение о месте нахождения ошибки, PolishArray тогда представляет пустой массив.

instance.doEval(parameter1:Array, parameter2:Array):Number – в качестве первого аргумента данный метод принимает свойство PolishArray объекта класса CompiledObject (который мы получили, использовав метод doCompile), а в качестве второго – массив численных значений переменных выражения; и возвращает численное значение выражения (записанного в массив PolishArray) при заданных значениях переменных.

myParser.doDeriv(myCompObj.PolishArray, [val]) – данный метод работает только для выражений с одной переменной. Он возвращает значение производной данного выражения при значении переменной равном значению val.

Общедоступных свойств у объектов данного класса нет.

Допустимый синтаксис

В анализируемых выражениях должен использоваться стандартный синтаксис, например: t^3+2.7*sin(pi*t)+e^(-t+1). В выражениях должны использоваться скобки вокруг аргументов функций, а также знак * для умножения, / – для деления и ^ – для возведения в степень.
Список функций и констант, которые анализатор "понимает":
- тригонометрические функции и обратные тригонометрические функции:
sin(), cos(), tan(), asin(), acos(), atan(). Например: tan(pi*t/3)+acos(t)
- натуральный логарифм – ln(), и основание натурального логарифма e.
Например: e^(2-t)*ln(t^2+1)
- квадратный корень – sqrt(), нахождение модуля – abs()
- выбор максимального значения – max(), выбор минимального значения – min() (оба работают для выбора из 2-х величин)
Например: max(sin(2*t), 0)
- для вычисления ближайшего меньшего целого, ближайшего большего целого и ближайшего целого используйте функции floor(), ceil() и round() соответственно.

вторник, 4 января 2011 г.

Использование класса MathParser

Этим постом я начну небольшую серию уроков-переводов. За основу взята 12 глава из книги "Flash and Math Applets: Learn by example", доступная здесь. Перевод этот неточный, специально переработанный для использования совместно с Flex SDK (в книге все примеры разработаны во Flash IDE), но достаточный для начала работы с описываемыми классами. Также хочу отметить, что на сайте книги есть документация к описываемым классам и множество других очень интересных примеров, которые могут оказаться полезными для flash-разработчиков, интересующихся математикой. Итак, первый пример...

Пример 1. Простые вычисления с помощью класса MathParser

Мы начнем с примера, разъясняющего работу с классом MathParser. В приложении, показанном на рисунке 1, пользователь вводит выражение, зависящее от значения переменной x, и численное значение этой переменной, при этом допустим такой синтаксис, как sqrt(2*pi) или e^2. Затем, для вычисления значения выражения, пользователь нажимает клавишу ввода.
Заметьте, что для вычисляемых выражений класс MathParser использует математические функции и константы языка ActionScript. Подобно Java, JavaScript, C++, и многим другим языкам программирования, математические функции ActionScript и константы поддерживают двойную степень точности (не уверен правильно ли я перевел термин double precision accuracy). Важно учитывать данный факт, т.к. это может привести к странным (неожиданным) результатам. Например, если вы попробуете вычислить значение выражения tan(pi/2), то (т.к. значение константы pi/2 не может быть выражено в численном виде абсолютно точно) вы получите значение тангенса при значении переменной x очень и очень близком к pi/2 (но не равном ему), а это очень большое число (хотя как мы знаем, что функция тангенса не определена при pi/2). Мораль здесь в том, что, чтобы создать точный научный калькулятор, нужно проделать намного больше работы, чем просто суметь вычислить значения встроенных функций и констант языка программирования.

Настройка сцены

Чтобы создать приложение, изображенное на рис. 1, сначала нам понадобится несколько элементов на сцене. Это должны быть 2 текстовых поля для ввода текста с именами inVal и inExp и динамическое текстовое поле txtResult. Остальную работу сделает скрипт.
Рис. 1. Простенький научный калькулятор (для просмотра щелкните по изображению)

Пишем код

Сначала мы импортируем класс MathParser, а также другие необходимые классы. Отметим, что папка flashandmath должна находиться в той же папке, что и исходный файл приложения.

package 
{
 import flashandmath.as3.parsers.*;

 import flash.display.Sprite;
 import flash.display.StageAlign;
 import flash.display.StageScaleMode;
 import flash.events.Event;
 import flash.events.KeyboardEvent;
 import flash.text.TextField;
 import flash.text.TextFieldAutoSize;
 import flash.text.TextFieldType;
 import flash.text.TextFormat;
 import flash.text.TextFormatAlign;
 import flash.ui.Keyboard;
 import teormech.utils.TextUtils;
 
 public class Main extends Sprite 
 {
 ... 
 
Затем мы создадим два анализатора синтаксиса, один для разбора выражений, зависящих от значения переменной x, а второй – для выражений, не содержащих переменных (т.е. для вычисления значения переменной x, при котором мы хотим найти значение первого выражения). Заметьте, что обязательным параметром конструктора класса MathParser является массив, содержащий имена переменных, которыми могут быть практически любые строки (кроме имен функций и констант). Для анализатора выражения, не содержащего переменных, мы, в качестве значения обязательного аргумента конструктора, передаем пустой массив [ ]. Класс MathParser входит в пакет flashandmath.as3.parsers, который мы импортировали ранее.
var mpExp:MathParser = new MathParser(["x"]); 
var mpVal:MathParser = new MathParser([ ]);
В классе MathParser синтаксический анализ выражения и вычисление значение выражения выделено в отдельные методы. Такое разделение обусловлено тем, что во многих приложениях необходимо разбирать выражение единожды, а вычислять его значение многократно (например, при разных значениях переменных). Для анализа выражений, мы объявляем две переменные, являющиеся экземплярами класса CompiledObject. Этот класс также входит в состав пакета flashandmath.as3.parsers.
var compobjExp:CompiledObject = new CompiledObject(); 
var compobjVal:CompiledObject = new CompiledObject(); 
Вычисление значения выражения вызывается нажатием на клавишу Enter (при нажатии на любую другую клавишу ничего происходить не будет). Данную задачу выполняет функция compute:

// Данная строка строка взята из тела функции init()
stage.addEventListener( KeyboardEvent.KEY_DOWN, compute); 
...
private function compute(kevt:KeyboardEvent):void { 
 if (kevt.keyCode != Keyboard.ENTER) { 
  return; 
 } 

 //  «Захват» содержимого текстовых полей
 var stVal:String = inVal.text; 
 var stExpression:String = inExp.text; 

 // «Компиляция» выражений, введенных пользователем
 compobjVal = mpVal.doCompile(stVal); 
 compobjExp = mpExp.doCompile(stExpression); 

 // Проверка на наличие синтаксических ошибок 
 // во введенных выражениях.
 // При возникновении ошибки в соответствующем 
 // текстовом поле выводится сообщение
 if (compobjVal.errorStatus == 1) { 
  txtResult.text = compobjVal.errorMes; 
  stage.focus = inVal; 
  return; 
 } 
 if (compobjExp.errorStatus == 1) { 
  txtResult.text = compobjExp.errorMes; 
  stage.focus = inExp; 
  return; 
 } 

 // Вычисление значения выражения переменной
 var xVal:Number = mpVal.doEval( compobjVal.PolishArray, [ ]); 
 // Вычисление значение выражения, 
 // зависящего от значения переменной
 var resVal:Number = 
  mpExp.doEval( compobjExp.PolishArray, [xVal]); 
 // Вывод ответа с точностью то десятых долей.
 txtResult.text = resVal.toFixed(10); 
}
Для вычисления сначала числового значения переменной x, а затем самого выражения с подстановленным значением переменной мы использовали метод doEval. Отметим, что первым аргументом метода doEval является свойство PolishArray соответствующего ему объекта класса CompiledObject, а вторым аргументом - массив чисел, которые нужно передать в выражение.

Так как выражение для анализатора mpVal не содержит переменных, то ему мы передали пустой массив, а для анализатора mpExp мы передали массив с одним элементом (содержащим значение этой переменной), т.к. это выражение содержит одну переменную.

Исходники к этому примеру можно взять здесь.

Пример 2. Использование класса MathParser для создания простой математической викторины

У класса MathParser может быть множество вариантов применения. Здесь мы рассмотрим простой пример, в котором осуществляется проверка ответа студента на вопросы с произвольно генерируемыми исходными данными (см. рис.2).

Рис. 2. Проверьте свои знания по школьной математике (для проверки щелкните по картинке)

Идея, лежащая в основе проверки вводимого ответа, такова: программа берет numpoints значений переменной (в данном случае переменной x), лежащих в промежутке между значениями min и max, и сравнивает результаты вычисления «правильного» выражения (которое выводит сама программа) с результатами вычисления выражения, введенного пользователем при выбранных значениях переменной. Если все результаты вычислений отличаются не более чем на величину tolerance, то ответ считается правильным. Хотя это и ненадежный механизм для проверки алгебраических ответов, он достаточно хорошо работает в данных условиях, когда мы знаем, что ответы являются простыми многочленами. Устанавливая значения переменных numpoints, min, max, и tolerance в начале программы, мы можем более точно ее настраивать для каждого конкретного случая.

Настройки сцены

Чтобы создать описываемое приложение, Вам понадобится расположить на сцене несколько объектов: это ярлыки (labels), динамические текстовые поля (txtInitial, txtRecursivePart, txtResponseBox) и текстовое поле для ввода текста пользовательского ответа txtUserAnswer, а также кнопки btnCheck и btnNext.

Пишем код

Сперва мы импортируем необходимые классы (MathParser в их числе) и далее задаем значения переменных-параметров нашей программы, предназначенных для проверки ответа:
package 
{
 import com.bit101.components.Label;
 import com.bit101.components.PushButton;
 import com.bit101.components.Style;
 import flash.display.Sprite;
 import flash.display.StageAlign;
 import flash.display.StageScaleMode;
 import flash.events.Event;
 import flash.events.KeyboardEvent;
 import flash.events.MouseEvent;
 import flash.text.TextField;
 import flash.text.TextFieldAutoSize;
 import flash.text.TextFieldType;
 import flash.text.TextFormat;
 import flash.text.TextFormatAlign;
 import flash.ui.Keyboard;
 import teormech.utils.TextUtils;
 
 import flashandmath.as3.parsers.*;
 
 public class Main extends Sprite 
 {
  
  private var txtPts:TextField;
  private var txtUserAnswer:TextField;
  private var txtResponseBox:TextField;
  
  private var btnCheck:PushButton;
  private var btnNext:PushButton;
  
  private var numpoints:Number = 12; 
  private var tolerance:Number = 0.0000001;
  private var min:Number = 2;
  private var max:Number = 5;

  private var stPt1:String;
  private var stPt2:String;
  private var stUserAnswer:String;
  private var stCorrectAnswer:String;
  ...
Функция nextProblem задает случайные исходные данные и правильный ответ для задачи. В качестве значений параметров задачи a, b, x1 и x2 выбран интервал целых чисел [-2; 5]. Эти параметры используются для определения значений y1 и y2 (так, чтобы через точки (x1; y1) и (x2; y2) проходила прямая, определяемая уравнением y = a*x + b) и построения правильного ответа – переменной stCorrectAnswer.
private function nextProblem():void
{
 var a:int;
 var b:int;
 var x1:int;
 var y1:int;
 var x2:int;
 var y2:int;
 /* 
 Выбираем случайные значения параметров для задачи, 
 лежащие в диапазоне {-2; 5}
 */
 a = 5 - Math.floor(8*Math.random());
 b = 5 - Math.floor(8*Math.random());
 x1 = 5 - Math.floor(8*Math.random());
 x2 = 5 - Math.floor(8*Math.random());
 if (x1 == x2) {
  x2 = x1 + 1;
 }
 y1 = a*x1 + b;
 y2 = a*x2 + b;
 
 stCorrectAnswer = String(a) + "*x + " + String(b);
   
 txtPts.text = "(" + String(x1) + ", " + String(y1) + 
 ") and (" + String(x2) + ", " + String(y2) + ").";
 
 txtResponseBox.text = "";
 txtUserAnswer.text = "";
}
Функция checkAnswer производит анализ ответа пользователя и правильного ответа как выражения, зависящего от переменной x, а затем сравнивает результаты их вычисления при различных значениях переменной x:
private function checkAnswer():void 
{
 // Проверка – был ли введен ответ. Если нет, 
 // то ничего не делаем 
 var stUserAnswer:String = txtUserAnswer.text;
 
 if (stUserAnswer.length == 0) {
  return;
 }

 // Осуществляем анализ введенного пользователем 
 // и правильного выражений
 var procFun:MathParser = new MathParser(["x"]);
 var corrCO:CompiledObject= procFun.doCompile(stCorrectAnswer);
 var userCO:CompiledObject = procFun.doCompile(stUserAnswer); 

 // Сообщаем об ошибке, если какое-либо из выражений 
 // содержит синтаксическую ошибку
 if (userCO.errorStatus == 1) {
  txtResponseBox.text = 
   "Ошибка синтаксиса в пользовательском ответе. " + 
   userCO.errorMes;
  return;
 }
   
 if (corrCO.errorStatus == 1) {
  txtResponseBox.text = 
   "Ошибка синтаксиса в программном ответе. " + 
   corrCO.errorMes;
  return;
 }
 /* Выбираем значения переменной x и интервала [min; max] 
 и находим значения y для выражений введенных пользователем 
 и правильного ответа. Если ответы различаются более, чем 
 на величину переменной tolerance, то мы знаем, что ответ
 неправильный.
 */  
 var thisX:Number;
 var userY:Number;
 var corrY:Number;
 var i:Number;
 
 for (i = 0; i < numpoints; i++) 
 {
  thisX = min + i*(max - min)/numpoints;
  userY = procFun.doEval(userCO.PolishArray,[thisX]);
  corrY = procFun.doEval(corrCO.PolishArray,[thisX]);
  
  if (Math.abs(userY - corrY) > tolerance) 
  {
   var incorrectTextFormat:TextFormat = 
    new TextFormat("Arial", "18", 0xff0000);
   txtResponseBox.text = "Неправильно";
   txtResponseBox.setTextFormat(incorrectTextFormat);
   return;
  }
 }
 // Если же мы не нашли ошибки, то ответ правильный  
 txtResponseBox.text = "Правильно!";
 txtResponseBox.setTextFormat(txtResponseBox.defaultTextFormat);
}
Далее мы добавляем обработчики событий, реагирующие на нажатие кнопки Проверить и клавиши Enter и вызывающие функцию проверки ответа:
btnCheck.addEventListener( MouseEvent.CLICK, btnCheckPressed); 
stage.addEventListener( KeyboardEvent.KEY_DOWN, keyPressed); 
...
private function btnCheckPressed(e:MouseEvent):void { 
 checkAnswer(); 
} 

function keyPressed(evt:KeyboardEvent):void { 
 // Проверка ответа срабатывает только при нажатии
 // на ENTER
 if (evt.keyCode == Keyboard.ENTER) { 
  checkAnswer(); 
 } 
}
Нажатие на кнопку Следующая задача должно вести к генерации условий следующей задачи путем вызова функции nextProblem. Также мы вызывает эту функцию при запуске приложения (внутри функции init.
nextProblem();
Исходники к этому примеру лежат здесь.

UPD: В исходных классах пакета flashandmath у многих переменных отсутствует типизация, если работать во Flash IDE, то никаких проблем не возникает, но если вы разрабатываете приложения с помощью Flex SDK, то компилятор ругается на отсутствие типизации переменных и идентификаторов, поэтому мне пришлось потратить немного времени для устранения этой проблемки. Классы пакета flashandmath, включенные в исходники к примерам отличаются от официальных лишь добавлением типизации.

суббота, 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, включающую в себя и другие элементы пользовательского интерфейса (текстовые поля, чекбоксы, переключатели и т.д.). Эта библиотека проста в использовании и довольно полезна при разработке прототипов (ускоряя ее). Думаю, что в следующих уроках опишу подробнее ее применение.

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


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

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