Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Последние темы форума

Показать новые сообщения »

Почтовая рассылка

Подписчиков: 11766
Последний выпуск: 19.06.2015

Программное рисование во Flash MX. Управление кривыми.

22 сентября 2005 года
Автор: Aib
Источник: http://www.flash-gorod.net/

Вместо вступления.

Наконец-то!!! Теперь во Flash MX мы можем рисовать по средствам программного кода: создавать и удалять клипы, делать различные градиентные заливки, свободно управлять размером, местоположением и прочими характеристиками текста и, конечно же, рисовать прямые и кривые линии.

Но вслед за восторгом от новшеств вскоре приходят некоторые проблемы. Кривая, заданая тремя точками (начальная является концом предыдущей линии или может быть задана с помощью метода moveTo), ведёт себя совершенно непонятно - через точку (ControlX, ControlY) она даже не проходит, и может возникнуть проблема с рисованием даже простой окружности.

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

Что рисует curveTo().

По сути данная кривая всего-навсего CV NURBS из трёх точек, каждая с весом 1, кривая степени 2. Для тех, кто изучал 3D-Studio MAX R2 или выше или какую-либо другую программу создания 3D-графики и анимации этого достаточно (Poser и Bryce3D в расчёт не беруться, т.к. Я их не видел и наличие там NURBS гарантировать не могу). Для всех остальных потребуются пояснения. Control Vertex Non-Uniform Rational Basic Splines (сокращённо CV NURBS) - это линии, состоящие из частей кривых различного порядка (т.е. заданых математическими уравнениями некоторой степени), форма которых определяется направляющими векторами, концы которых находятся в задаваемых пользователем точках. В Flash MX эти составляющие имеют степень 2, т.е. это могут быть параболы, гиперболы и эллипсы. Но так как вес точки (коэффициэнт, влияющий на то, как близко к этой точке подходит кривая) постоянен и равен единице, то линия, создаваемая curveTo() является частью параболы. Из этого следует, что просто нарисовать обычную окружность или эллипс нельзя, а можно лишь нарисовать кривую, которая будет на них похожа. Ввиду ограниченности кол-ва задаваемых точек, сложную кривую нужно создавать по частям, рассчитывая точки так, чтобы не было изломов. Однако есть и свои плюсы. Направляющие векторы в нашем случае - это касательные к параболе в двух точках её графика, а точка (ControlX, ControlY) - пересечение этих касательных.

Это позволяет получить условие построения более сложной кривой без изломов. Для этого нужно, чтобы контрольная (сontrol) и якорная (anchor) точки первой кривой и контрольная точка второй кривой находились на одной прямой.

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

Метод рисование сложной кривой multicurveTo().

Данный метод позволит нам рисовать почти полноценный CV NURBS. Почти, потому что мы не сможем менять вес точки (всегда равный единице) и устанавливать степень кривой больше 2. Будет ещё одна оговорка, но о ней чуть позднее. Итак, программный код метода:

function Multicurve(Xargs, Yargs, closed)
{
	var Xmid = Xargs.slice(0);
	var Ymid = Yargs.slice(0);
	if ((Xmid.length != Ymid.length) || (Xmid.length < 2))
	{
		trace('Wrong Arguments');
		return this
	};

	if (Xmid.length == 2)
	{
		this.moveTo(Xmid[0], Ymid[0]);
		this.lineTo(Xmid[1], Ymid[1]);
		delete Xmid;
		delete Ymid;
		return this;
	};

	var Xpoint = new Array();
	var Ypoint = new Array();
	for (var i = 1; i < Xmid.length-2; i++)
	{
		Xpoint[i] = 0.5*(Xmid[i+1]+Xmid[i]);
		Ypoint[i] = 0.5*(Ymid[i+1]+Ymid[i]);
	};

	if (closed)
	{
		Xpoint[0] = 0.5*(Xmid[1]+Xmid[0]);
		Ypoint[0] = 0.5*(Ymid[1]+Ymid[0]);
		Xpoint[i] = 0.5*(Xmid[i+1]+Xmid[i]);
		Ypoint[i] = 0.5*(Ymid[i+1]+Ymid[i]);
		Xpoint[i+1] = 0.5*(Xmid[i+1]+Xmid[0]);
		Ypoint[i+1] = 0.5*(Ymid[i+1]+Ymid[0]);
		Xmid[i+2] = Xmid[0];
		Ymid[i+2] = Ymid[0];
		Xpoint[i+2] = Xpoint[0];
		Ypoint[i+2] = Ypoint[0];
	}
	else
	{
		Xpoint[0] = Xmid[0];
		Ypoint[0] = Ymid[0];
		Xpoint[i] = Xmid[i+1];
		Ypoint[i] = Ymid[i+1];
		Xmid.pop();
		Ymid.pop();
	};

	this.moveTo (Xpoint[0], Ypoint[0]);
	for (var i = 1; i < Xmid.length; i++)
	{
		this.curveTo(Xmid[i], Ymid[i], Xpoint[i], Ypoint[i]);
	};

	delete Xmid;
	delete Ymid;
	delete Xpoint;
	delete Ypoint;
	return this;
}

Object.prototype.multicurveTo = Multicurve;

Теперь разберём всё по порядку:

function Multicurve(Xargs, Yargs, closed){

В качестве аргументов передаём два массива - координаты контрольных точек кривой по х и по у, а также параметр closed, который показывает, какую мы хотим получить кривую. Если кривая замкнута, то линия не имеет начала и конца, все заданные точки используются как контрольные (значение closed равно true), в противном случае нулевая (исходя из индекса в массиве) и последняя заданные точки являются началом и концом кривой(значение closed равно false). Можно было бы сделать проверку на замкнутость автоматической, но иногда бывают случаи, когда нужно, чтобы линия имела совпадающие начало и конец.

var Xmid = Xargs.slice(0);
var Ymid = Yargs.slice(0);

Возможно, Вам в дальнейшем потребуются массивы, которые вы задали как параметры функции. Если производить с аргументами Xargs и Yargs какие-нибудь преобразования, то изменятся и сами передаваемые массивы. Поэтому создаются их локальные копии (на локальность указывает слово var). Если Вы уверены, что передаваемые массивы нигде больше использоваться не будут, можно удалить эти две строчки, а первую строку записать как function Multicurve(Xmid, Ymid, closed){.

if ((Xmid.length != Ymid.length) || (Xmid.length < 2)){
trace('Wrong Arguments');
return this
};

Проверка на отсутствие ошибок. Если задано меньше двух точек, или количество координат по X не равно количеству координат по Y, то метод заканчивается с передачей ссылки на себя и выводит в режиме теста соответствующее сообщение. Если проверка наличия хотя-бы двух точек - простая формальность, то проверка различного числа координат может быть весьма полезной. При желании строчку trace('Wrong Arguments'); можно заменить на что-нибудь вроде trace('Wrong Arguments: '+(Xmid.length-Ymid.length)), чтобы знать, сколько и каких координат не достаёт. if (Xmid.length == 2){ this.moveTo(Xmid[0], Ymid[0]); this.lineTo(Xmid[1], Ymid[1]); delete Xmid; delete Ymid; return this; };

Ещё одна проверка. Если задано всего две точки, то мы просто рисуем соединяющий их отрезок и заканчиваем выполнение метода. Данная проверка обязательна, т.к. иначе используемый далее метод curveTo() просто не получит достаточное количество параметров. Слово this используется для того, чтобы данный метод, применённый к любому клипу перенимал все его свойства и действовал в его пределах. Обращаю Ваше внимание на следующие строки:

delete Xmid;
delete Ymid;

В них удаляются из памяти локальные массивы. В ActionScript эти строчки не обязательны, т.к. "сборщик мусора" сам с ними разберётся.

Теперь вся подготовительная работа выполнена и можно начинать создание массивов данных для построения кривой:

var Xpoint = new Array();
var Ypoint = new Array();
for (var i = 1; i < Xmid.length-2; i++){
Xpoint[i] = 0.5*(Xmid[i+1]+Xmid[i]);
Ypoint[i] = 0.5*(Ymid[i+1]+Ymid[i]);
};

Создаём ещё два локальных массива, Xpoint и Ypoint, которые будут содержать координаты всех якорных точек. Далее в цикле происходит рассчёт координат этих якорных точек. Здесь и появляется та самая оговорка. В оригинале в CV NURBS координаты таких точек зависят от того, насколько далеко от концов кривой они находятся. Но подобный способ получения координат во Flash себя не оправдывает, и вот почему. В нашем методе мы создаём якорные точки на середине отрезка, соединяющего соседние контрольные точки. Максимальное отклонение "реального" местоположения якорных точек от середины отрезка меньше 0,58 % длины самого отрезка. Так что этим вполне можно пренебречь. Отмечу, что рассчёт координат происходит для точек с первой по предпоследнюю, так как координаты первой и последней рассчитываются в зависимости от того, замкнута кривая или нет.

if (closed){
Xpoint[0] = 0.5*(Xmid[1]+Xmid[0]);
Ypoint[0] = 0.5*(Ymid[1]+Ymid[0]);
Xpoint[i] = 0.5*(Xmid[i+1]+Xmid[i]);
Ypoint[i] = 0.5*(Ymid[i+1]+Ymid[i]);
Xpoint[i+1] = 0.5*(Xmid[i+1]+Xmid[0]);
Ypoint[i+1] = 0.5*(Ymid[i+1]+Ymid[0]);
Xmid[i+2] = Xmid[0];
Ymid[i+2] = Ymid[0];
Xpoint[i+2] = Xpoint[0];
Ypoint[i+2] = Ypoint[0];
}

Если кривая замкнута, то мы создаём нулевую якорную точку на середине отрезка между нулевой и первой контрольными точками, последнюю - между предпоследней и последней. И нам потребуются координаты ещё нескольких якорных точек: точки, находящейся на середине отрезка, соединяющего последнюю контрольную точку с первой, и копии координат нулевых якорной и контрольной точек. Это делается для того, чтобы объединить в один код рисование замкнутой и не замкнутой кривых. Используемая в индексах переменная i после окончания цикла имела значение Xmid.length-2, поэтому, следуя нашей организации массивов, Xpoint[i], Ypoint[i] - это координаты якорной точки, находящейся между предпоследней и последней контрольными (если здесь вообще уместно говорить, какая точка первая, а какая - последняя), Xpoint[i+1], Ypoint[i+1] - между последней и первой, Xpoint[i+2], Ypoint[i+2] - копии координат.

} else {
Xpoint[0] = Xmid[0];
Ypoint[0] = Ymid[0];
Xpoint[i] = Xmid[i+1];
Ypoint[i] = Ymid[i+1];
Xmid.pop();
Ymid.pop();
};

Если же кривая не замкнута, то мы просто делаем нулевую и последнюю контрольные точки нулевой и последней якорной. Снова используем переменную i: Xmid[i+1], Ymid[i+1] являются поледними элементами массивов, т.к. i+1 = Xmid.length-1. Далее мы удаляем последний элемент в массивах координат контрольных точек, но оставляем первый затем, чтобы каждой паре якорных точек [i-1], [i] соответствовала [i]-ая контрольная точка, как и в случае замкнутой кривой.

Нахождение координат точек завершено и начинается процесс рисования кривой:

this.moveTo (Xpoint[0], Ypoint[0]);
for (var i = 1; i < Xmid.length; i++){
this.curveTo(Xmid[i], Ymid[i], Xpoint[i], Ypoint[i]);
};

Сначала указываем начальную точку.

Затем начинаем рисовать кривые, где для каждой i-ой кривой начальная точка имеет координаты (Xpoint[i-1], Ypoint[i-1]), контрольная - (Xmid[i], Ymid[i]), а конечная - (Xpoint[i], Ypoint[i]). Искомая линия построена!!!

delete Xmid;
delete Ymid;
delete Xpoint;
delete Ypoint;
return this;
}

Удаление массивов и окончание метода. Без комментариев.

В последней строчке Object.prototype.multicurveTo = Multicurve; мы добавляем наш метод к набору имеющихся методов класса Object.

В завершении...

Метод описан, можете смело его использовать. Для простоты рекомендую поместить весь программный код в текстовый файл с расширением .as и в каждом новом клипе, где этот метод может пригодиться, в корне (_root) написать строку

#include path

где path - это абсолютный или относительный путь к файлу с кодом. Заметьте, что после #include точка с запятой НЕ ставится. Лично у меня это выглядит так:

#include "D:/Flash/aibdraw.as"

Если для написания программного кода вы используете встроенный во Flash редактор, то, возможно, Вам пригодиться следущее:

  • Заходите в корневой каталог Flash, далее \First Run\ActionsPanel
  • Открываете файл ActionsPanel.xml, находите <ifdef mode="FEATURE_DRAWING_API">
  • Перед </folder> прописываете подобную строчку: <string name="multicurveTo" tiptext="Draws a multipoint curve from the first to the last point" object="MovieClip" text=".multicurveTo(% [pointsX], [pointsY], Closed? %)" type="procedure" version="6" />

После этого в левом столбике редактора ActionScript и всплывающих подсказках появится multicurveTo().


"Кривые, кривые, кривые... Это, конечно, хорошо, но как насчёт чего-го конкретного? К примеру, как нарисовать самую элементарную окружность?" Вопрос действительно хороший. Если каждый раз рисовать окружность вручную, задавая поэтапно якорные и контрольные точки curveTo() (а для хорошей точности нужно от 6 до 18 кривых!), то очень скоро можно прийти к выводу, что делается это как-то по другому. И первое, что приходит на ум, написать метод, который бы рисовал окружность по заданному радиусу и координатам центра, говоря другими словами, создать прототип (это математический термин, не надо его путать с понятием прототипа объекта в ООП!).

В двух словах о создании прототипа. Вообще говоря, можно выделить две группы. Представители первой не зависят от параметров. Вызывая метод рисования, Мы всегда получаем один и тот же объект в одном и том же месте клипа. Затем все преобразования делаются по средствам перемещения, маштабирования и поворота клипа, который содержит этот объект. Это может быть полезно, к примеру, при создании кнопок, чекбоксов и т.п. Для создания такого прототипа достаточно просто скопировать код рисования понравившегося объекта в метод, добавив перед вызовами методов lineTo(), curveTo() и т.д. ключевое слово this. Всё просто. Вторая группа поинтереснее. В неё входят прототипы объектов, которым необходимо передавать параметры. К примеру, спираль имеет определённое количество витков, дуга - определённый угол, прямоугольник - радиус закригления углов, звезда - количество лучей и т.д. Если Вам нужна спираль, которая используется несколько раз с различным количеством витков, то бессмысленно создавать отдельные прототипы для каждой спирали, и эффективнее создать прототип, отвечающий всему классу данных спиралей (данных, потому что спирали бывают разные по способу задания и, соответственно, по внешнему виду). Но в подобных случаях простым копированием блока программного кода не обойтись. Потребуется придумать некий алгоритм рисования. При создании алгоритма очень полезной может оказаться математическая формула объекта, если таковая существует. Иногда проще бывает создать прототипы отдельных частей, а затем соединить их вместе.

Есть ещё одна деталь. Иногда (на практике - довольно часто) бывает нужно передавать в качестве параметров угол наклона или маштаб. Это нужно тогда, когда необходимо, чтобы внутри одного клипа было нарисовано несколько объктов разного размера и под разным углом.

Но слова это только слова, поэтому далее Я хотел бы проэллюстрировать всё вышесказанное на конкретном примере.

Метод рисования эллипса ellipseTo().

Итак, окружность или дуга, а лучше эллипс, и хорошо бы, чтоб его можно было его рисовать под углом к осям координат. Немного о самом алгоритме. Если бы у Вас была нарисована окружность, то чтобы получить эллипс Вы бы растянули клип, в котором находится окружность, чтобы повернуть его изменили свойство _rotation, чтобы подвинуть задали новые координаты клипа. Если мы поместим внутрь клипа с координатами (0;0) вместе с окружностью необходимые для её построения контрольные(control) и якорные(anchor) точки, затем растянем этот клип, повернём и переместим, чтобы получить искомый объект, глобальные координаты контрольных и якорных точек и будут искомыми (т.е., если рисовать по точкам с этими координатами кривую, то мы опять же получим искомый объект). Именно таким образом мы будем находить координаты контрольной или якорной точки: сначала для окружности, потом, после растяжения, для эллипса, потом для повёрнутого эллипса, и, наконец, для перемещённого. Теперь, после определения алгоритма осталось самая малость - написать программный код. Ну как, приступаем?

Для начала опишем метод:

movieClip.prototype.ellipseTo = function(){
var t = arguments[0][0] == undefined ? 1 : 0;
var CenterX = arguments[0+t][0];
var CenterY = arguments[0+t][1];
var Dir = 1;
var ARadius;
var BRadius;
var StartAngle = 0;
var EndAngle = 0;
var ARadAngle = 0;

Кому-то это может показаться непонятным: почему в круглых скобках после имени метода ничего нет, откуда взялся массив arguments, да ещё и двумерный, и как при таком описании этот метод вызывать??? Сейчас всё разъясню.

Наш метод - это достаточно мощный инструмент. Как уже было выше сказано, с его помощью можно будет рисовать эллипс (или его часть) под заданным углом к осям координат. А для этого нам нужно передать функции координаты центра, длины радиусов эллипса, начальную и конечную точки дуги (в данном примере эти параметры задаются углами между прямой, проходящей через начало (конец) дуги и центр эллипса, и горизонтальной осью координат) и угол между главной осью эллипса и горизонтальной осью координат. Кроме того, в некоторых случаях бывает нужно не перемещать начальную точку для рисования дуги с помощью moveTo(), а начать рисовать с того места, где закончилась предыдущая линия. Стало быть нужен флаг. Всего восемь параметров. Но, допустим, нам нужна просто окружность, для которой достаточно трёх параметров (координаты центра и радиус). И что тогда делать с остальными параметрами? Поэтому было бы хорошо сделать наш метод таким, чтобы он мог работать с разным количеством параметров.

Наш метод будет иметь 8 аргументов: Dir(флаг, показывающий, используется ли moveTo() в начале рисования и в каком направлении нужно рисовать эллипс) [CenterX, CenterY] (координаты центра), [ARadius, BRadius] (радиусы эллипса), [StartAngle, EndAngle] (начало и конец дуги в градусах) и ARadAngle (угол между главной осью эллипса, длина которой задаётся параметром ARadius, и горизонтальной осью координат). Именно в таком порадке. Из них параметры Dir, [StartAngle, EndAngle] и ARadAngle могут быть не заданы, кроме того, если нужна окружность, вместо массива параметров [ARadius, BRadius] можно передать один параметр Radius. К примеру, следующий набор аргументов

(1, [100, 200], 50, [45, 270])

будет означать, что нужно нарисовать дугу от 45 до 270 градусов, с центром в точке (100, 200) и радиусом 50, moveTo() не требуется и строить её нужно в направлении отсчёта углов. А этот набор

([200, 150], [100, 80], 70)

будет означать, что нужен эллипс с радиусами (100, 80), с центром в точке (200, 150) и наклонённый под углом 70 градусов.

Теперь о массиве arguments. Этот массив создаётся автоматически для каждого метода и содержит все его аргументы. Двумерным он получился потому, что некоторые из передаваемых аргументов в свою очередь являются массивами (к примеру, [CenterX, CenterY]). Я использовал массив arguments, а не описал все эти аргументы в круглых скобках, потому что в программе будет происходить изменение переменных StartAngle и EndAngle, и если бы мы описали их в круглых скобках (..., [StartAngle, EndAngle],...), то изменения отразились бы и на передаваемом массиве. Если же отказаться от использования массива, то нельзя будет сделать автоопределение того, хотим ли мы целую фигуру или только дугу. Кроме того, в любом случае потребуется переход из градусной меры в радианную.

Переменная t принимает значение 1, если был задан флаг Dir и 0, если нет. Если НЕ был, то считается, что moveTo() используется. Эта переменная также показывает, с какого места в массиве arguments следуют все остальные данные. К примеру, если флаг не задан, то массив [CenterX, CenterY] находится в ячейке arguments[0], а если задан - то arguments[1]. Поэтому мы просто прибавляем переменную к номеру ячейки (в этом же примере arguments[0+t]).

И ещё немного о смысле флага Dir и его применении в нашем методе. Зачем он вообще нужен? Дело в том, что если при использовании заливки применить метод moveTo(), то предыдущая линия автоматически замыкается. Этот эффект не всегда желателен, и в этих случаях moveTo() не используется (как правило, оно и не нужно, так как при обресовке границы заливки дуга начинается там, где закончилась предыдущая линия). С другой стороны, когда заливка не используется, и нужно просто нарисовать пару отдельных окружностей, нет никакой необходимости вручную указывать место начала рисования линии. Теперь о значениях, которые в данном примере может принимать флаг. В процессе рисования границы заливки может также оказаться важным, в каком направлении происходит рисование дуги. На практике это проявляется в том, какой конец дуги должен примыкать к уже имеющейся части контура. Поэтому Я решил использовать флаг и для определения направления: значение 1 по направлению отсчёта углов, -1 - против. Значение по умолчанию равно 1, т.е. если moveTo() используется, то рисуем в направлении отсчёта углов.

Итак, мы определили все параметры. Но значения присвоили только пяти, да и то три из них обнулили. Дело в том, что координаты центра [CenterX, CenterY] существуют независимо от того, что мы хотим получить: окружность, эллипс или дугу. Параметрам StartAngle, EndAngle и ARadAngle присваиваются значения "по умолчанию". Почему именно такие, будет видно далее.

В процессе вычислений нам потребуются ещё две переменные, которым при описании также задаём некоторые значения по умолчанию:

var div = 12;
var delta = Math.PI/6;

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

with (Math){
...............
............... //программный код 
...............
}

Функция with здесь действует совершенно также, как и с клипами. Если вызывается метод или переменная без указания родителя, то Flash сначала просматривает все методы и переменные, описанные внутри блока, а затем, если не находит, ищет их в указанном классе (в данном случае Math).

ВАЖНО!!! Использование with облегчает написание кода, но сильно замедляет выполнение программы компьютером. В статье Я использовал with для облегчения восприятия и повышения читабельности кода. Если для Вас высокая скорость не является важной, то можете использовать with, но для повышения производительности лучше пару минут понажимать Ctrl+v и установить везде прямые ссылки. В исходнике, прилагающемся к статье, метод описан без использования with.

Начнём определять значение остальных параметров. Итак, сначала выясним, в каком направлении нужно рисовать, если направление было задано:

if (t == 1 && (arguments[0] == -1 || arguments[0] == 1)){
Dir = arguments[0];
}

Напомню, что если при вызове метода мы задаём флаг Dir, то считается, что начальная точка дуги задаётся концом предыдущей линии и использовать moveTo() для её получения не нужно. Здесь же проверяем, правильно ли задан флаг.

Теперь определим, что мы рисуем - эллипс или окружность:

if (arguments[1+t][0] == undefined){
ARadius = BRadius = arguments[1+t];
} else {
ARadius = arguments[1+t][0];
BRadius = arguments[1+t][1];
};

Проверяем, является ли второй аргумент массивом. Если нет, то значение первого элемента массива, которого нет, естественно равно undefined, задан один радиус и тогда оба радиуса эллипса принимают одно значение. Если да, то переменной ARadius присваиваем значение первого элемента массива, BRadius - значение второго.

Теперь разберёмся с параметрами StartAngle, EndAngle и ARadAngle:

with (Math){ 
if (arguments.length > 2+t){
if (arguments[2+t][0] == undefined){
ARadAngle = PI*arguments[2+t]/180;
} else {
StartAngle = PI*arguments[2+t][0]/180;
EndAngle = PI*arguments[2+t][1]/180;

Первая строчка проверяет, заданы ли ещё какие-нибудь аргументы, кроме первых двух/трёх (массив координат центра и массив радиусов эллипса/радиус окружности, плю учитываем флаг Dir). Если условие выполнено, то проверяем, является ли третий аргумент массивом. Если нет, то это угол между главной осью эллипса и горизонтальной осью координат. Присваиваем его значение переменной ARadAngle, переведённое из градусной меры в радианную, и процесс передачи аргументов метода его локальным переменным на этом закончен. А вот если третий аргумент - массив, то это значения углов начала и конца дуги. Присваиваем их переменным StartAngle и EndAngle (снова предварительно переводя их из градусной меры в радианную) и затем...

Затем следует фрагмент кода, в котором происходит преобразование значений углов начала и конца дуги.

if (ARadius != BRadius){
if (cos(StartAngle) < 0){
StartAngle = PI+atan(ARadius*tan(StartAngle)/BRadius);
} else if (cos(StartAngle) > 0){
StartAngle = atan(ARadius*tan(StartAngle)/BRadius);
};
if (cos(EndAngle) < 0){
EndAngle = PI+atan(ARadius*tan(EndAngle)/BRadius);
} else if (cos(EndAngle) > 0) {
EndAngle = atan(ARadius*tan(EndAngle)/BRadius);
};
};

Вспомним, как устроен наш алгоритм: сначала строим окружность (здесь и далее, когда в тексте будут упоминаться окружность, дуга или эллипс и преобразования, связанные с ними, подразумевается, что речь идёт о контрольных и якорных точках), затем растягиваем окружность по горизонтали до эллипса, поворачиваем эллипс на заданный угол и в конце переносим его из начала координат в заданную точку (CenterX, CenterY). На стадии маштабирования значения углов меняются. Поэтому мы сразу меняем значения начального и конечного углов дуги, чтобы после маштабирования получить исходный угол. Преобразования имеет смысл делать только в том случае, если у нас эллипс, поэтому проверяется неравенство радиусов.

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

Итак, пусть задан угол U. Если произвести обратное преобразование эллипса в окружность, то получится некоторый угол V. Значит, если в начале задать вместо угла U угол V, то после растягивания мы снова получим угол U. Вычислим угол V. tan(V) = A/B; tan(U) = A/C; C = B*k, где k - коэффициэнт растяжения, равный отношению горизонтального радиуса к вертикальному (k = ARadius/BRadius). Отсюда tan(V) = tan(U)*C/B = tan(U)*B*k/B = tan(U)*k. Именно из этого соотношения берётся формула StartAngle = atan(ARadius*tan(StartAngle)/BRadius) для начального угла и аналогичная для конечного. Теперь о проверках, связанных со знаком cos. tan(PI/2) и tan (3*PI/2) не существуют (равны бесконечности), cos(PI/2)=cos(3*PI/2)=0. Однако, если угол U=PI/2 или 3*PI/2, при растяжении окружности вдоль горизонтальной оси его значение не изменится. Поэтому случай, когда cos(StartAngle) = 0 мы сразу убираем. Проверка формальна, так как скорее всего, если попросить Flash вывести значение tan(PI/2), вы получите что-то вроде 1.63317787283838e+16, большое, но всё же конечное число. Если cos(StartAngle) Ну, со сложными математическими преобразованиями пока всё. Возвращаемся к программному коду:

while (!(EndAngle > StartAngle)){
StartAngle -= 2*PI;
};
while (EndAngle-StartAngle > 2*PI){
EndAngle -= 2*PI;
}; 

Дуга строится всегда от начального угла к конечному, поэтому если значение начального угла не меньше конечного, то мы уменьшаем StartAngle на 2*PI радиан за шаг, пока оно не станет меньше EndAngle (забудем пока о том, что построение может идти и в противоположном напрпвлении - от большего угла к меньшему). Второй цикл следит за тем, чтобы разница между углами не была больше 2*PI радиан.

Теперь вспомним про описанные вначале переменные div и delta:

div = ceil(6*(EndAngle-StartAngle)/PI);
delta = (EndAngle-StartAngle)/div;

Переменная div показывает, какое количество дуг нам нужно взять при условии, что каждая из них не больше PI/6 радиан (в случае, когда углы не заданы как аргументы метода, div = (2*PI-0)/(PI/6) = 12, что является её значением по умолчанию). А переменная delta - сколько радиан точно составляет каждая дуга (по умолчанию delta = (PI*2-0)/12 = PI/6). Как Я уже говорил в первой части статьи, из параболы эллипс можно получить только с определённой долей приближения. Чем больше кривых curveTo() мы используем, тем более точное получаем приближение. Если для создания дуги в PI/6 радиан будет использована одна curveTo(), то точность приближения будет 927/1000, что Я считаю вполне достаточным. Если вам этого мало (много), просто разделите в строке div = ceil(6*(EndAngle-StartAngle)/PI) выражение (EndAngle-StartAngle) на меньший (больший) угол, а также измените значения по умолчанию div и delta. Скажу лишь, что если на PI/36 радиан отводить одну кривую curveTo(), то точность будет составлять 9997/10000. Так что брать ещё меньший угол вряд ли имеет смысл.

И в завершении передачи аргументов для случая рисования дуги:

if (arguments.length == 4){
ARadAngle = PI*arguments[3+t]/180;
};
};
};

Проверка, задан ли четвёртый аргумент - угол наклона. И закрывающие фигурные скобки.

Итак, на данный момент код программы выглядит так:

movieClip.prototype.ellipseTo = function(){
var t = arguments[0][0] == undefined ? 1 : 0;
var CenterX = arguments[0+t][0];
var CenterY = arguments[0+t][1];
var Dir = 1;
var ARadius;
var BRadius;
var StartAngle = 0;
var EndAngle = 0;
var ARadAngle = 0;
var div = 12;
var delta = Math.PI/6;
if (t == 1 && (arguments[0] == -1 || arguments[0] == 1)){
Dir = arguments[0];
}
if (arguments[1+t][0] == undefined){
ARadius = BRadius = arguments[1+t];
} else {
ARadius = arguments[1+t][0];
BRadius = arguments[1+t][1];
};
with (Math){ 
if (arguments.length > 2+t){
if (arguments[2+t][0] == undefined){
ARadAngle = PI*arguments[2+t]/180;
} else {
StartAngle = PI*arguments[2+t][0]/180;
EndAngle = PI*arguments[2+t][1]/180;
if (ARadius != BRadius){
if (cos(StartAngle)&lt 0){
StartAngle = PI+atan(ARadius*tan(StartAngle)/BRadius);
} else if (cos(StartAngle)> 0){
StartAngle = atan(ARadius*tan(StartAngle)/BRadius);
};
if (cos(EndAngle)&lt 0){
EndAngle = PI+atan(ARadius*tan(EndAngle)/BRadius);
} else if (cos(EndAngle)> 0) {
EndAngle = atan(ARadius*tan(EndAngle)/BRadius);
};
};
while (!(EndAngle > StartAngle)){
StartAngle -= 2*PI;
};
while (EndAngle-StartAngle > 2*PI){
EndAngle -= 2*PI;
}; 
div = ceil(6*(EndAngle-StartAngle)/PI);
delta = (EndAngle-StartAngle)/div;
if (arguments.length == 4){
ARadAngle = PI*arguments[3+t]/180;
};
};
}; 

Подготовка данных окончена. Приступаем к нахождению координат контрольных и якорных точек. Нам потребуются ещё две константы, радиусы эллипса, которому принадлежат все контрольные точки, участвующие в построении:

var ABRadius = ARadius/cos(delta/2);
var BBRadius = BRadius/cos(delta/2);

Чтобы понять, почему все они лежат на одном эллипсе, опять придётся ненадолго углубится в математику (желающие могут снова пропустить пару абзацев). Вспомним, как создаётся наш эллипс: сначала находим координаты контрольных и якорных точек для окружности, затем растягиваем по X, затем поворачиваем и в конце переносим. Итак, пусть у нас есть часть окружности:

Угол U - это наша delta. После задания параметров методу выяснилось, что для построение дуги потребуется две curveTo(). В общем случае их может быть от одной до двенадцати (полная окружность или эллипс), но на данном этапе нам потребуется всего одна, так что их общее количество пока не важно. Для начала найдём угол V. Отрезки A2_C1 и A1_C1 являются направляющими векторами для curveTo, поэтому они являются касательными, в данном случае к заданной окружности. А значит они перпендекулярны радиусам O_A2 и O_A1 соответственно по свойству касательных к окружности. Значит, треугольники O_A2_C1 и O_A1_C1 прямоугольные. Они имеют одну общую сторону O_C1, а стороны O_A2 и O_A1 равны как радиусы окружности. Значит, эти треугольники равны, и угол V = V1. В сумме они дают угол U, а значит угол V = U-V1 = U-V = U/2. Теперь найдём O_C1. По определению cos(V) = A1_C1/O_C1. Отсюда O_C1 = A1_C1/cos(U/2). Отсюда получается формула BBRadius = BRadius/cos(delta/2). Как видно, при постоянных BRadius и delta, BBRadius также будет постоянен. BBRadius - расстояние от центра окружности до любой участвующей в построении контрольной точки. Значит, все контрольные точки равноудалены от центра заданной окружности, т.е. сами лежат на окружности с тем же центром и радиусом BBRadius. Теперь, если мы рассмотрим соседнюю дугу, то для неё направляющий вектор curveTo A2_C2 также будет перпендекулярен O_A2. Тем самым выполняется условие отсутствия изломов: контрольная и якорная точки первой кривой и контрольная точка второй кривой находятся на одной прямой.

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

Поэтому после растяжения они также будут находится на некотором эллипсе. Его горизонтальный радиус ABRadius = BBRadius*k = (BRadius/cos(delta/2))*(ARadius/BRadius) = ARadius/cos(delta/2), так как коэффициэнт растяжения у них одинаковый. Так как при растяжении прямые остаются прямыми (меняются лишь углы между ними), то условие отсутствия изломов по-прежнему сохраняется.

Итак, начинаем нахождение координат точек! Чтобы не делать два цикла и не заводить новые массивы с данными, мы будем находить координаты контрольной и якорной точек и сразу вызывать curveTo(). Но сначала, если флаг Dir не был задан, нужно найти координаты стартовой точки, к которой мы перейдём через moveTo():

if (t==0){
var teX = ARadius*cos(StartAngle);
var teY = BRadius*sin(StartAngle);
var X1 = CenterX+teX*cos(ARadAngle)-teY*sin(ARadAngle);
var Y1 = CenterY+teX*sin(ARadAngle)+teY*cos(ARadAngle);
this.moveTo (X1, Y1);
} 

Если бы точка лежала на окружности с центром в начале координат, то её координаты были бы (BRadius*cos(StartAngle), BRadius*sin(StartAngle)). Так как в общем случае мы производим растяжение, то координату X нужно домножить на ARadius/BRadius. Итак, переменные teX и teY содержат координаты стартовой точки на неповёрнутом эллипсе с центром в начале координат.

Теперь нужно выполнить поворот. Для этого используется формула:

// X' = X*cos(u)-Y*sin(u)
// Y' = X*sin(u)+Y*cos(u)

где X, Y - старые координаты точки, X', Y' - новые, u - угол поворота. Желающие могут посмотреть коротенький вывод этих формул:

Итак, координаты точки А (X, Y). Пусть длина отрезка О_А равна L. Тогда X=L*cos(u); Y=L*sin(u). Теперь производим поворот на угол V. Точка A перешла в A' с координатами X'=L*cos(u+v); Y'=L*sin(u+v). Теперь раскрываем скобки по формулам синуса и косинуса суммы: X'=L*cos(u+v)=L*cos(u)*cos(v)-L*sin(u)*sin(v)=X*cos(v)-Y*sin(v), аналогично X'=L*sin(u+v)=L*sin(u)*cos(v)+L*cos(u)*sin(v)=Y*cos(v)+X*sin(v).

Последнее, что осталось сделать, это переместить точку в соотвествии с заданным центром эллипса. Для этого просто прибавляем к координатам точки соответствующие значения координат центра.

Теперь чуть-чуть опртимизируем код.

if (t==0){
var te = ARadius*cos(StartAngle);
var Y1 = BRadius*sin(StartAngle);
var X1 = CenterX+teX*cos(ARadAngle)-teY*sin(ARadAngle);
Y1 = CenterY+teX*sin(ARadAngle)+teY*cos(ARadAngle);
this.moveTo (X1, Y1);
}

В качестве второй временной переменной используем Y1, значение которой изменяется лишь в конце этой части кода. Ещё одна вещь: если Вы привыкли, что отсчёт углов происходит против часовой стрелки, а не по часовой, то нужно как-бы перевернуть ось Y, и пятая строчка тогда будет выглядеть так:

Y1 = CenterY-te*sin(ARadAngle)-Y1*cos(ARadAngle);

И, наконец, учитывая, что переменнные X1, Y1 и te потребуются в цикле независемо от того, используем мы moveTo() или нет, делаем окончательный вариант:

var X1;
var Y1;
var te; 
if (t==0){
te = ARadius*cos(StartAngle);
Y1 = BRadius*sin(StartAngle);
X1 = CenterX+teX*cos(ARadAngle)-teY*sin(ARadAngle);
Y1 = CenterY+teX*sin(ARadAngle)+teY*cos(ARadAngle);
this.moveTo (X1, Y1);
}

Теперь, если флаг Dir всё же был задан и равен -1, то нужно поменять местами значения начального и конечного углов:

else if (Dir==-1){
te = StartAngle;
StartAngle = EndAngle;
EndAngle = te;
}

В цикле потребуются ещё две переменные для хранения координат контрольных точек. Пока просто опишем их:

var X2;
var Y2;

Ну, все преготовления закончились. Запускаем цикл рисования эллипса:

for (var i = 1; i&lt=div; i++){
te = ARadius*cos(StartAngle+Dir*delta*i);
Y1 = BRadius*sin(StartAngle+Dir*delta*i);
X1 = CenterX+te*cos(ARadAngle)-Y1*sin(ARadAngle);
Y1 = CenterY+te*sin(ARadAngle)+Y1*cos(ARadAngle);
te = ABRadius*cos(StartAngle+Dir*delta*(i-0.5));
Y2 = BBRadius*sin(StartAngle+Dir*delta*(i-0.5));
X2 = CenterX+te*cos(ARadAngle)-Y2*sin(ARadAngle);
Y2 = CenterY+te*sin(ARadAngle)+Y2*cos(ARadAngle);
this.curveTo (X2, Y2, X1, Y1);
};
};
}

Цикл пройдёт div раз и создаст div дуг. Сначала находим координаты для якорных точки. Всё происходит по тому же принципу, что и для начальной точки . На каждом этапе работы цикла мы смещаемся на угол delta относительно предыдущего положения, в конце концов достигая конечного угла EndAngle = StartAngle+Dir*delta*div. Переменная Dir указывает, нужно прибавлять угол или вычитать, в завсимости от выбранного направления построения.

Координаты контрольных точек находятся аналогично якорным, так как тоже находятся на эллипсе. Угол берётся на delta/2 меньше, чем для якорной точки (это следует из проведённых ранее вычислений). Опять же, если нужно другое направление отсчёта,

Y1 = CenterY-te*sin(ARadAngle)-Y1*cos(ARadAngle)
Y2 = CenterY-te*sin(ARadAngle)-Y2*cos(ARadAngle); 

И, наконец, вызов метода curveTo(), ради которого были все наши труды.

ВСЕ!!! Создание окончено. Целиком код метода выглядит так:

movieClip.prototype.ellipseTo = function(){
var t = arguments[0][0] == undefined ? 1 : 0;
var CenterX = arguments[0+t][0];
var CenterY = arguments[0+t][1];
var Dir = 1;
var ARadius;
var BRadius;
var StartAngle = 0;
var EndAngle = 0;
var ARadAngle = 0;
var div = 12;
var delta = Math.PI/6;
with (Math){ 
if (arguments[1+t][0] == undefined){
ARadius = BRadius = arguments[1+t];
} else {
ARadius = arguments[1+t][0];
BRadius = arguments[1+t][1];
};
if (arguments.length > 2+t){
if (arguments[2+t][0] == undefined){
ARadAngle = PI*arguments[2+t]/180;
} else {
StartAngle = PI*arguments[2+t][0]/180;
EndAngle = PI*arguments[2+t][1]/180;
if (ARadius != BRadius){
if (cos(StartAngle)&lt 0){
StartAngle = PI+atan(ARadius*tan(StartAngle)/BRadius);
} else if (cos(StartAngle)> 0){
StartAngle = atan(ARadius*tan(StartAngle)/BRadius);
};
if (cos(EndAngle)&lt 0){
EndAngle = PI+atan(ARadius*tan(EndAngle)/BRadius);
} else if (cos(EndAngle)> 0) {
EndAngle = atan(ARadius*tan(EndAngle)/BRadius);
};
};
while (!(EndAngle > StartAngle)){
StartAngle -= 2*PI;
};
while (EndAngle-StartAngle > 2*PI){
EndAngle -= 2*PI;
}; 
div = ceil(6*(EndAngle-StartAngle)/PI);
delta = (EndAngle-StartAngle)/div;
if (arguments.length == 4){
ARadAngle = PI*arguments[3+t]/180;
};
};
};
var te; 
var X1;
var Y1;
var X2;
var Y2;
if (t==0){
te = ARadius*cos(StartAngle);
Y1 = BRadius*sin(StartAngle);
X1 = CenterX+teX*cos(ARadAngle)-teY*sin(ARadAngle);
Y1 = CenterY+teX*sin(ARadAngle)+teY*cos(ARadAngle);
this.moveTo (X1, Y1);
} else if (Dir==-1){
te = StartAngle;
StartAngle = EndAngle;
EndAngle = te;
}
for (var i = 1; i&lt=div; i++){
te = ARadius*cos(StartAngle+Dir*delta*i);
Y1 = BRadius*sin(StartAngle+Dir*delta*i);
X1 = CenterX+te*cos(ARadAngle)-Y1*sin(ARadAngle);
Y1 = CenterY+te*sin(ARadAngle)+Y1*cos(ARadAngle);
te = ABRadius*cos(StartAngle+Dir*delta*(i-0.5));
Y2 = BBRadius*sin(StartAngle+Dir*delta*(i-0.5));
X2 = CenterX+te*cos(ARadAngle)-Y2*sin(ARadAngle);
Y2 = CenterY+te*sin(ARadAngle)+Y2*cos(ARadAngle);
this.curveTo (X2, Y2, X1, Y1);
};
};
} 

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

Надеюсь, что данная статья была для Вас полезной.

В завершении хочу выразить благодарность Nox Noctis'у - за предпросмотр и критику.

До свидания! Удачи!

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

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог