Программное рисование во Flash MX

Программное рисование во Flash MX

Возможности рисования в ActionScript были введены только в последней версии Flash. И буквально через месяц после ее выхода на специализированных сайтах в Сети появилась огромная масса поражавших воображение эффектов. Это были и трехмерные вращающиеся в пространстве фигуры, и яркие и при этом размером менее килобайта заставки и даже 3D-лабиринты а-ля Doom. Несомненно, с появлением функций рисования возможности для творчества существенно расширились (см. рис.1).



Рис.1. Созданная авторами сфера (ее расположение в пространстве задается произвольно)

Рисовать трехмерные объекты, подобные приведенной на рис.1. сфере, совсем не трудно (некоторые сложности может вызвать лишь необходимый для этого математический аппарат). Однако начнем мы с самого простого — нарисуем квадрат.

Прежде, чем взять в руки кисть, любой художник должен позаботиться о холсте. Методы рисования относятся к классу MovieClip — следовательно, в качестве такового должен быть использован клип, основная временная шкала или же кнопка. В общем, программно рисовать можно там же, где и вручную. Обычно же в качестве холста применяется динамически создаваемый чистый видеоклип. Подобный подход имеет несколько неоспоримых достоинств. Во-первых, он позволяет менять положение, поворот и размер графики, не влияя на остальные объекты. Во-вторых, при этом не приходится хранить клипы-холсты в библиотеке, что уменьшает размер конечного файла.

Итак, вводим новый пустой клип:

_root.createEmptyMovieClip("my_mc",0);

Когда холст будет создан, системе нужно сообщить, какой линией должно проводиться рисование. Сделать это можно при помощи метода lineStyle(thickness, rgb, alpha), где:

  • thickness — толщина линии. Может быть задана целым числом от 0 до 255. Значению 0 соответствует hairline — самая тонкая из возможных во Flash линий. Если данный параметр (или сам метод lineStyle) не прописан, линии рисоваться не будут.
  • rgb — параметр цвета линии. Необязателен. Принимает шестнадцатеричный код (или порядковый номер) желаемого оттенка. Если же он не задан, то по умолчанию линии рисуются черными.
  • alpha — необязательный параметр, определяющий прозрачность линии. Принимает значение в процентах: 0 — абсолютно прозрачная линия, 100 — совершенно непрозрачная. Если параметр не задан, то ему автоматически ставится в соответствие значение 100.
  • Пожалуй, наш квадрат будет иметь красный непрозрачный контур, образованный линией толщиной в 3 единицы:

    my_mc.lineStyle(3,0xFF0000,100);

    Как мы рисуем карандашом? Мы выбираем точку, с которой должна начаться линия, и опускаем его острие на бумагу. Проведя его по нужной траектории, мы переходим к новой линии. Подобным образом рисует и Flash. Поэтому вначале ему необходимо указать, в какой точке следует опустить перо. Сделать это можно при помощи метода moveTo(x,y), где x и y — координаты точки, с которой начнется линия.

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

    my_mc.moveTo(250,200);

    Провести прямой отрезок между последней точкой линии (или точкой опускания пера) и некоторой произвольной точкой можно, используя метод lineTo(x,y), где x и y — координаты этой точки.

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

    my_mc.lineTo(350,200);

    my_mc.lineTo(350,300);

    my_mc.lineTo(250,300);

    my_mc.lineTo(250,200);

    Вот и все. Квадрат готов. Правда, очень просто?

    При программном рисовании к клипу-холсту приходится применять одновременно большое количество методов (даже в простейшем примере создания квадрата их понадобилось шесть). Несколько упростить написание кода в таких случаях можно, используя предложение with(object){statements}, где object — объект, которому должны быть поставлены в соответствие некоторые действия, statements — сами действия. Применяя with, вы можете писать код без непосредственных указаний на объект, методы или свойства которого должны быть использованы. Например, рисующий квадрат скрипт при этом запишется как:

    _root.createEmptyMovieClip("my_mc",0);

    with(my_mc){

    lineStyle(3, 0xFF0000, 100);

    moveTo(250, 200);

    lineTo(350, 200); lineTo(350, 300);

    lineTo(250, 300); lineTo(250, 200);

    }

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

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

    Col=new Color("my_mc");

    Col.setRGB("0x00FF00");

    То, что квадрат может быть изображен при помощи прямых линий, это очевидно. А вот реально ли, используя их, нарисовать круг? Безусловно — только отрезки должны быть очень маленькими (замена гладких кривых ломаными — это очень распространенный в математике ход).

    Чтобы изобразить окружность, совсем необязательно знать ее уравнение и, тем более, прописывать отдельно (подобно примеру с квадратом) положение каждой точки. Для этого достаточно вспомнить, что чертится она полным поворотом циркуля. Следовательно, любая точка окружности может быть описана через радиус и угол, на который нужно повернуть циркуль, чтобы попасть в нее (см. рис.2).



    Рис.2. Параметрическое описание окружности

    Из чертежа 2 следует, что координаты точки окружности радиуса R с центром в точке 0(x,y) могут быть вычислены как:

    X=x+R´fSymbol;cos(a ) и Y=y- R´fSymbol;sin(a )

    Последовательно соединяя точки с координатами, полученными с использованием данных формул при изменении (с достаточно малым шагом) переменной (от 0 до 2´fSymbol;p ), мы нарисуем желаемую фигуру:

    var R=100; // радиус окружности

    var X0=200; // координата X центра окружности

    var Y0=200; // координата Y центра окружности

    var N=100; // количество отрезков, из которых должна быть построена окружность

    _root.createEmptyMovieClip("roun",0); // создаем холст

    with(roun){ // используем предложение with для упрощения набора кода

    lineStyle(2,0xFF0000,100); // окружность будет рисоваться красной непрозрачной линией толщиной в 2 px

    moveTo(X0+R*Math.cos(0),Y0+R*Math.sin(0)); // опускаем перо в начальной точке рисования (a =0)

    // последовательно соединяем все N точек, соответствующих изменению (от 0 до 2p

    for(var alpha=0; alpha<=2*Math.PI; alpha+=2*Math.PI/N){

    lineTo(X0+R*Math.cos(alpha),Y0-R*Math.sin(alpha));

    }

    }

    В зависимости от того, каким вы выберете N, у вас будут получаться в большей или меньшей степени гладкие фигуры (см. рис.3).



    Рис.3. Зависимость гладкости окружности от числа отрезков

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

    Спираль описывается теми же уравнениями, что и окружность, правда, с переменным радиусом. Если R уменьшается, то спираль сходится, если увеличивается — расходится. Если мы хотим, чтобы она как бы "накручивалась" на точку, то начальное значение ее радиуса должно быть равно нулю:

    var R=0;

    Чтобы получить красивую спираль, радиус-вектор нужно обернуть не менее 5-10 раз (a =10-20p ). Начальное же значение для угла поворота должно быть равно 0:

    var alpha=0;

    Спираль мы будем проводить синей линией толщиной в два пикселя:

    _root.lineStyle(2,0x0000DD,100);

    В данном примере мы будем рисовать, используя в качестве холста основную временную шкалу. Так как код расположен на одном из ее кадров, писать вначале методов "_root" совсем необязательно: если целевой объект не указан, интерпретатор в качестве такового использует ту временную шкалу, на которой находится соответствующий скрипт. Но мы все же будем это делать — для большей ясности.

    Начнем рисовать спираль мы приблизительно с середины рабочего поля:

    _root.moveTo(200,200);

    Теперь мы должны решить, каким способом лучше вызывать чертящий спираль код — используя обработчик события onEnterFrame или функцию setInterval()? Ввиду того, что никаких других объектов, кроме программно создаваемой графики, в фильме не будет, первый вариант представляется более простым:

    _root.onEnterFrame=function(){}

    Параметрические уравнения, описывающие спираль, по виду идентичны использованным нами ранее для построения окружности:

    _root.lineTo(200+R*Math.cos(alpha),200-R*Math.sin(alpha));

    Когда отрезок будет проведен, значение угла поворота должно быть увеличено на шаг. Чем меньше он будет, тем более гладкой получится кривая, но и тем дольше она будет строиться:

    alpha+=0.1;

    Чтобы у нас получилась спираль, а не окружность, каждый ее новый элементарный фрагмент должен отстоять от центра дальше предыдущего. А для этого всякий раз, когда он проводится, R должен быть увеличен (чем больше будет шаг такого увеличения, тем большее расстояние будет разделять витки спирали):

    R+=0.1;

    Вот и все. Спираль готова (см. рис.4).



    Рис.4. Спираль

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

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

    Между _root и клипом нет, в принципе, особой разницы. Наиболее существенное различие состоит в том, что система координат основной временной шкалы является строго фиксированной, а положение точки отсчета клипа можно легко менять, используя инструмент Transform (Трансформация). Из-за того же, что центр _root расположен в верхнем левом углу рабочего поля, изменение значения свойства _rotation приведет к повороту спирали относительно него, а не собственной оси. При попытке же увеличить размеры фигуры она просто "уедет". Вывод: основная временная шкала — это не лучший вариант для холста, и нам следует использовать в качестве него более пластичный объект.

    Обычно для программного рисования принято создавать пустые клипы при помощи метода createEmptyMovieClip(). Однако для нашего случая такой подход ничуть не лучше применения _root, так как центром пустого видеоклипа является верхний левый угол области фильма.

    Остается единственный вариант. Создайте новый клип и сохраните его пустым. Перетащите его экземпляр и, назвав mov, разместите ровно посередине поля.

    В созданном ранее коде нужно заменить _root на mov. Наиболее просто это сделать, воспользовавшись командой Replace (Заменить) окна Actions (Действия).

    Так как центр спирали должен совпадать с точкой отсчета клипа mov, опустить перо необходимо в положении (0,0) относительно его системы координат:

    mov.moveTo(0,0);

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

    mov.lineTo(R*Math.cos(alpha),R*Math.sin(alpha));

    Далее в обработчик события onEnterFrame необходимо добавить строки, описывающие задуманные трансформации:

    mov._rotation-=7; // направление вращения должно совпадать с направлением "роста" спирали

    mov._width+=0.5;

    mov._height+=0.5;

    Тестируем фильм. Замечательный получился эффект, не правда ли (особенно с учетом того, что занимает он всего 300 байт)? Чтобы он смотрелся еще лучше, увеличьте частоту смены кадров до 24 в секунду.

    Плоская спираль — это, конечно, интересно. А можно ли во Flash начертить спираль в пространстве? Непосредственным образом — нет, так как у нас в распоряжении всего две координаты. Но создать зрительную иллюзию все же реально.

    Знаете ли вы универсальный принцип, по которому создаются трехмерные эффекты на компьютере? Главная его идея — объем существует, но видим мы лишь его проекцию на плоскость нашего зрения. Соответственно, чтобы нарисовать любой пространственный объект, его необходимо описать как трехмерный, а перед выводом на экран спроецировать, следуя выбранному расположению системы координат.

    Перейти от спирали двухмерной к трехмерной очень просто. Для этого достаточно просто связать координату Z с линейно изменяющейся со временем переменной. В нашем случае в качестве последней мы можем использовать радиус R:

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

    var X=R*Math.cos(alpha);

    var Y=R*Math.sin(alpha);

    var Z=R;

    Когда координаты очередной узловой точки будут определены, можно выполнять проецирование:

    mov.lineTo(X-Z/2,Y+Z/2);

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

    Тестируем фильм. Да, спираль действительно чертится в пространстве, но расположена она относительно наблюдателя как-то не очень удачно (рис.5 (a)). Нужно каким-то образом ее развернуть. Но как это сделать?

    Поворот системы координат описывается чрезвычайно громоздкими формулами, приводить которые в этой статье вряд ли стоит. Однако обращение фигуры можно сделать достаточно легко, просто поменяв выражения для координат местами. Возможных вариантов расположения при этом возникает шесть (вдоль каждой из полуосей), и среди них обязательно должен оказаться приемлемый для нас. Так, достаточно неплохо смотрится спираль, получающаяся при обмене формулами между переменными X и Z (рис.5 (b)).



    Рис.5. Пространственные спирали

    Трудно спорить с тем, что при условии достаточно малого шага гладкие кривые можно строить и при помощи прямых отрезков. Но подобный ход эффективен лишь только тогда, когда фигура может быть описана математически. В этом случае построение проводится динамически, и число узловых точек объекта никак не влияет на размер swf-файла. Так, совершенно неважно, сколько витков будет иметь наша спираль — 1 или 1000 — размер фильма в любом случае будет всего 300 байт. Но представьте, что вам надо нарисовать, например, лицо героя. При этом, естественно, вы не сможете воспользоваться никакими уравнениями, и каждую точку придется описывать отдельно. Страшно даже подумать, какой объем работы придется выполнить, чтобы задать при помощи черточек длиной в несколько пикселей даже самый примитивный рисунок. Вывод: должен существовать способ, позволяющий задавать кривые за меньшее число шагов, чем при использовании метода lineTo().

    Если бы при рисовании вручную Flash создавал изображение при помощи прямых черточек, то размер swf-файлов был бы в десятки и сотни раз большим, чем он есть на самом деле. Однако изучаемая нами программа использует куда как более эффективный подход. Кривые составляются не из прямых отрезков (получаясь, по сути, ломаными), а из фрагментов гладких кривых, описывающихся алгебраическими полиномами второй и третьей степени (т.н. сплайны). При этом для задания фигуры без обилия мелких деталей требуется записывать намного меньше информации, чем при ее построении из прямых черточек.

    ActionScript предоставляет возможность программного рисования при помощи сплайнов благодаря наличию специального метода curveTo(controlX, controlY, anchorX, anchorY). Данный метод рисует фрагмент параболы второго порядка так, чтобы он, начавшись в последней точке кривой (или точке опускания пера), заканчивался в точке с координатами X=anchorX, Y=anchorY. Направление и степень растяжения сплайна определяется параметрами controlX и controlY, которые соответствуют координатам точки пересечения касательных к параболе в якорных точках (см. рис.6).



    Рис.6. Принцип построения кривой при помощи curveTo()

    Если говорить объективно, то построение кривой при помощи участков парабол — это не самый лучший из возможных вариантов. Гораздо более универсален подход, использующий обобщенное уравнение кривых второго порядка, так как при этом сплайны могут быть фрагментами не только парабол, но и эллипсов и гипербол. А еще лучше применять кубические сплайны, позволяющие воспроизводить и колебания участка кривой. Однако эти методы требуют задания большого числа параметров, поэтому использовать их достаточно сложно. Так что curveTo() является золотой серединой между примитивным lineTo() и наиболее эффективными кубическими сплайнами.

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

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

    function circle(radius, NumberOfSteps, Xcenter,Ycenter, thickness, rgb, alpha){ }

    Большинство методов ActionScript имеют как обязательные, так и необязательные для задания параметры. Если последние не определены, то используются принятые по умолчанию установки. Очевидно, что наш метод будет куда удобнее, если будет позволять задавать лишь необходимые аргументы.

    Если пользователь посчитает излишним задавать отдельные параметры, построение должно быть проведено исходя из наиболее общих установок. Для того, чтобы их прописать, мы создадим специальный массив, последовательность элементов которого будет совпадать с последовательностью записи аргументов в скобках функции circle. По умолчанию круг будет иметь радиус, равный 50 пикселям, и центр в точке (0,0). Рисоваться он будет за пять шагов черной непрозрачной линией толщиной в два пикселя:

    Arr=[50,10,0,0,2,0,100];

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

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

    if(radius!=undefined){}

    Однако такой подход является слишком громоздким. Можно поступить и проще, обратившись к свойству arguments.

    Свойство arguments хранит массив с переданными функции аргументами. Используя его, мы можем обратиться к ее параметрам, не прописывая по отдельности имя каждого из них. Так, чтобы заменить установки по умолчанию на переданные величины, достаточно запустить цикл из семи итераций, присваивающий i-тому элементу массива Arr значение i-того элемента массива arguments в том случае, если последний существует:

    for (var i = 0; i<7; i++) {

    if (arguments[i]!= undefined) {

    Arr[i] = arguments[i];

    }

    }

    Если в качестве параметров построения окружности мы используем ссылки на элементы массива Arr, читабельность кода будет невысока. Поэтому имеет смысл создать для наиболее важных из них специальные переменные:

    var R = Arr[0], X0 = Arr[2], Y0 = Arr[3];

    var step = 2*Math.PI/Arr[1]; // шаг для переменной угла поворота параметрических уравнений окружности

    Задаем стиль линии и опускаем перо (так как мы заранее не знаем, к какому клипу будет применен наш метод, то в качестве объекта для методов рисования нужно использовать объект-местоимение this):

    this.lineStyle(Arr[4], Arr[5], Arr[6]);

    this.moveTo(X0+R, Y0); // координаты точки начала рисования получаем, положив угол поворота в параметрических уравнениях окружности равным 0

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

    //так как первая точка уже задана в методе moveTo(), то отсчет угла нужно начать сразу со значения step

    for (var phi=step; phi<=2*Math.PI; phi += step) {}

    Метод curveTo() требует задания якорной и направляющей точек. И если с первой никаких сложностей не будет, так как она лежит на окружности и может быть подсчитана точно так же, как ранее мы определяли параметры для lineTo(), то для вычисления второй придется немного заняться математикой (см. рис.7).



    Рис.7. Построение окружности при помощи фрагментов парабол

    Направляющей точке на приведенном чертеже соответствует точка B. Чтобы найти ее координаты, нужно использовать те же уравнения, что и для якорных точек. Параметрический радиус будет постоянен и равен длине отрезка OB (обозначим ее как R1), а угол поворота будет на полшага меньше значения переменной цикла:

    var ControlX=X0+R1*Math.cos(phi-step/2);

    var ControlY=Y0-R1*Math.sin(phi-step/2);

    var AnchorX=X0+R*Math.cos(phi);

    var AnchorY=Y0-R*Math.sin(phi);

    this.curveTo(ControlX, ControlY, AnchorX, AnchorY);

    Вычислить длину OB совсем несложно. Так как отрезок ОС является радиусом окружности, то он будет перпендикулярен касательной CB. Следовательно, треугольник COB является прямоугольным. Тогда гипотенуза OB может быть найдена как отношение катета OC к косинусу прилежащего к нему острого угла, равного половине величины шага:

    var R1 = R/Math.cos(step/2);

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

    Вот и все. Функция, рисующая круг, готова. Однако пока она не является методом ни одного класса. Чтобы ее можно было использовать в качестве такового, она должна быть добавлена в число наследуемых классом возможностей. Сделать это можно, применив специальный элемент ActionScript prototype:

    Object.prototype.circleTo=circle;

    Так как окружность должна рисоваться как в клипах, так и в кнопках, метод circleTo нужно поставить в соответствие классу Object. Это наиболее общий класс ActionScript, и все остальные классы являются его подклассами. Следовательно, любой метод, присущий Object, наследуется и всеми остальными классами.

    Задуманный метод создан. Он ничем не отличается от таких встроенных методов, как getDepth() или hitTest(), и может быть использован по отношению к любому клипу или кнопке, расположенным на любой временной шкале фильма.

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

    _root.circleTo(100,15,200,200,4,0x00BB00,100);

    Ура! Получилось! Окружность выглядит просто идеально!

    Поэкспериментируйте немного с созданным методом. Вы увидите, что чем большим задается число шагов, тем лучше (при постоянном радиусе) выглядит окружность. Хороший результат достигается уже при 8 шагах, что в пять раз меньше, чем при использовании lineTo(). Согласитесь, что 20 точек прописать проще, чем 50. А для отдельных случаев это различие может быть еще больше!



    Рис.8. Влияние числа шагов на качество получаемой фигуры

    Для отработки техники использования метода curveTo() перепишите функцию circle() так, чтобы при помощи нее можно было строить спирали. Это очень просто и должно обязательно у вас получится. Если же вы чувствуете, что у вас есть определенные математические способности, попробуйте написать метод, при помощи которого можно будет чертить, используя curveTo, пространственные спирали. Если получится — можете собой гордиться!

    Дмитрий Гурский, Юрий Стрельченко, dot@omen.ru


    Компьютерная газета. Статья была опубликована в номере 10 за 2003 год в рубрике soft :: графика

    ©1997-2024 Компьютерная газета