Пример: анимация вращающейся луны



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

  • Загрузка внешнего изображения и доступ к данным без сжатия.

  • Создание анимации путем многократного копирования пикселов из разных частей исходного изображения.

  • Создание растрового изображения путем установки значений пикселов

Файлы приложений для этого примера можно найти по адресу: www.adobe.com/go/learn_programmingAS3samples_flash_ru. Файлы приложений анимированной вращающейся луны можно найти в папке Samples/SpinningMoon. Приложение состоит из следующих файлов.

Файл

Описание

SpinningMoon.mxml

или

SpinningMoon.fla

Основной файл приложения в Flex (MXML) или Flash (FLA).

com/example/programmingas3/moon/MoonSphere.as

Класс, отвечающий за загрузку, отображение и анимацию луны.

moonMap.png

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

Загрузка внешнего изображения в растровом формате

Первой ключевой задачей в данном примере является загрузка внешнего файла изображения с фотографией лунной поверхности. Операция загрузки обрабатывается двумя методами класса MoonSphere: конструктором MoonSphere(), который инициирует процесс загрузки, и методом imageLoadComplete(), который вызывается после полной загрузки внешнего изображения.

Загрузка внешнего изображения похожа на загрузку внешнего SWF-файла: и в том, и в другом случае для загрузки используется экземпляр класса flash.display.Loader. Реальный код метода MoonSphere(), который начинает загрузку, выглядит следующим образом:

var imageLoader:Loader = new Loader(); 
imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoadComplete); 
imageLoader.load(new URLRequest("moonMap.png"));

В первой строке объявляется экземпляр Loader с именем imageLoader. Третья строка фактически начинает процесс загрузки, вызывая метод load() объекта Loader, передающий экземпляр URLRequest с URL-адресом загружаемой картинки. Во второй строке задается прослушиватель событий, который будет вызван после полной загрузки изображения. Обратите внимание, что метод addEventListener() вызывается не в самом экземпляре Loader, а наоборот, в свойстве contentLoaderInfo объекта Loader. Сам по себе экземпляр Loader не отправляет события, связанные с загружаемым содержимым. Однако его свойство contentLoaderInfo содержит ссылку на объект LoaderInfo, связанный с загружаемым в объект Loader содержимым (в данном случае, внешним изображением). Объект LoaderInfo располагает несколькими событиями, относящимися к процессу и завершению загрузки внешнего содержимого, включая событие complete (Event.COMPLETE), которое вызывает метод imageLoadComplete(), когда изображение полностью загрузится.

Несомненно, начало загрузки внешнего изображение является важной частью процесса, но не менее важно знать, что делать после окончания загрузки. Как видно из приведенного выше кода, после загрузки изображения вызывается функция imageLoadComplete(). Она выполняет ряд операций над загруженными данными изображения. Они описаны далее в соответствующих разделах. Тем не менее, для использования данных ей необходимо иметь к ним доступ. Когда для загрузки внешнего изображения используется объект Loader, загружаемое изображение становится экземпляром Bitmap, присоединенным в качестве дочернего экранного объекта Loader. В этом случае метод прослушивателя событий будет подключаться к экземпляру Loader как часть объекта события, который передается методу в качестве параметра. Метод imageLoadComplete() начинается со следующих строк:

private function imageLoadComplete(event:Event):void 
{ 
    textureMap = event.target.content.bitmapData; 
    ... 
}

Обратите внимание, что параметр объекта события называется event и является экземпляром класса Event. Каждый экземпляр класса Event имеет свойство target, относящееся к объекту, который вызывает событие (в данном случае, это экземпляр LoaderInfo, в котором вызывается метод addEventListener(), как описано выше). Объект LoaderInfo, в свою очередь, имеет свойство content, которое (после окончания процесса загрузки) содержит экземпляр Bitmap с загруженным растровым изображением. Если вам нужно вывести изображение непосредственно на экран, можно присоединить этот экземпляр Bitmap (event.target.content) к контейнеру экранного объекта. (Также можно присоединить объект Loader к контейнеру экранного объекта.) Тем не менее, в данном примере загруженное содержимое используется в качестве данных изображения без сжатия, а не для вывода на экран. Следовательно, в первой строке метода imageLoadComplete() содержится свойство bitmapData загружаемого экземпляра Bitmap (event.target.content.bitmapData), сохраняемое в переменной экземпляра под названием textureMap. Эта переменная, как описано в следующем разделе, используется для создания анимации вращения луны.

Создание анимации путем копирования пикселов

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

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

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

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

textureMap = event.target.content.bitmapData;

Содержание textureMap — это показанное выше изображение. Кроме того, для создания анимации вращения в примере используется экземпляр Bitmap с именем sphere, который является экранным объектом, выводящим изображение луны на экран. Как и textureMap, объект sphere создается и заполняется исходными данными изображения в методе imageLoadComplete() с помощью следующего кода:

sphere = new Bitmap(); 
sphere.bitmapData = new BitmapData(textureMap.width / 2, textureMap.height); 
sphere.bitmapData.copyPixels(textureMap, 
                         new Rectangle(0, 0, sphere.width, sphere.height), 
                         new Point(0, 0));

Как видно из кода, создается экземпляр sphere. Его свойство bitmapData (данные изображения без сжатия, отображаемые экземпляром sphere), создается с тем же значением высоты и вполовину меньшим значением ширины textureMap. Иными словами, содержимое sphere будет иметь тот же размер, что и фотография луны (так как изображение textureMap содержит две расположенные рядом фотографии луны). Затем свойство bitmapData заполняется данными изображения с помощью метода copyPixels(). Параметры метода copyPixels() могут указывать на несколько моментов.

  • Первый параметр указывает, что данные изображения копируются из textureMap.

  • Второй параметр (новый экземпляр Rectangle) указывает, из какой части textureMap будет сделан снимок. В данном случае, снимком будет прямоугольник в левом верхнем углу textureMap, заданном первыми двумя параметрами Rectangle(): 0, 0, а ширина и высота снимка будут соответствовать свойствам width и height экземпляра sphere.

  • Третий параметр (новый экземпляр Point со значениями x и y, равными 0) определяет целевое расположение данных пиксела: в данном случае, это верхний левый угол (0, 0) sphere.bitmapData.

Можно наглядно видеть, как код копирует пикселы из объекта textureMap, показанного на рисунке ниже, и вставляет их в sphere. Иными словами, содержимое BitmapData объекта sphere является частью объекта textureMap, которая выделена на рисунке.

Не забывайте, что это лишь начальное состояние объекта sphere — первое изображение, которое копируется в объект sphere.

Когда исходное изображение загружено, а объект sphere создан, остается последняя задача — создать анимацию. Она решается методом imageLoadComplete(). Анимацией управляет экземпляр Timer с именем rotationTimer, который создается и запускается с помощью следующего кода:

var rotationTimer:Timer = new Timer(15); 
rotationTimer.addEventListener(TimerEvent.TIMER, rotateMoon); 
rotationTimer.start();

Сначала создается экземпляр Timer с именем rotationTimer. Параметр, передаваемый конструктору Timer(), указывает, что экземпляр rotationTimer должен вызывать событие timer каждые 15 миллисекунд. Затем вызывается метод addEventListener() и указывает, что при выполнении события timer (TimerEvent.TIMER) вызывается метод rotateMoon(). Наконец, таймер запускается через вызов метода start().

Из-за способа определения rotationTimer проигрыватель Flash вызывает метод rotateMoon() в классе MoonSphere примерно каждые 15 миллисекунд, и именно таким образом осуществляется анимация луны. Исходный код метода rotateMoon() показан ниже:

private function rotateMoon(event:TimerEvent):void 
{ 
    sourceX += 1; 
    if (sourceX > textureMap.width / 2) 
    { 
        sourceX = 0; 
    } 
     
    sphere.bitmapData.copyPixels(textureMap, 
                                    new Rectangle(sourceX, 0, sphere.width, sphere.height), 
                                    new Point(0, 0)); 
     
    event.updateAfterEvent(); 
}

Код выполняет три задачи.

  1. Значение переменной sourceX (изначально равно 0) увеличивается на 1.

    sourceX += 1;

    Позже вы увидите, что sourceX используется для указания расположения объекта textureMap, из которого пикселы копируются в объект sphere, поэтому с помощью данного кода мы смещаем прямоугольник в объекте textureMap на один пиксел вправо. Вернемся к визуальному представлению. Через несколько циклов анимации исходный прямоугольник сместится вправо на несколько пикселов, как показано ниже.

    Еще через несколько циклов он сместится еще дальше.

    Это постепенное, равномерное смещение области, из которой копируются пикселы, — ключ к эффекту анимации. Медленно и равномерно смещая исходную область вправо, мы добиваемся эффекта вращения объекта sphere, который отображается на экране, влево. По этой причине требуется две копии исходного изображения поверхности луны (textureMap). Так как прямоугольник непрерывно движется вправо, большую часть времени оказывается выделенной область, расположенная не на одной копии фотографии, а захватывающая и ту, и другую копии.

  2. С движением прямоугольника исходной области вправо связана одна сложность. Рано или поздно прямоугольник достигнет края изображения textureMap, и пикселы лунной поверхности, которые нужны для копирования в объект sphere, закончатся.

    Эта проблема решается с помощью следующих строк кода:

    if (sourceX >= textureMap.width / 2) 
    { 
        sourceX = 0; 
    }

    Код следит, когда sourceX (левый край прямоугольника) достигнет середины объекта textureMap. Когда это происходит, значение sourceX снова сбрасывается до 0, прямоугольник смещается к левому краю textureMap, и цикл начинается заново:

  3. После вычисления нужного значения sourceX остается последний этап — создание анимации для копирования новых пикселов исходного прямоугольника в объект sphere. Используемый для этого код похож на тот, с помощью которого заполняется объект sphere (приводился ранее). Единственное отличие — в данном случае, при вызове конструктора new Rectangle() левый край прямоугольника помещается в точку sourceX:

    sphere.bitmapData.copyPixels(textureMap, 
                                new Rectangle(sourceX, 0, sphere.width, sphere.height), 
                                new Point(0, 0));

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

Создание сферической формы

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

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

moonMask = new Shape(); 
moonMask.graphics.beginFill(0); 
moonMask.graphics.drawCircle(0, 0, radius); 
this.addChild(moonMask); 
this.mask = moonMask;

Обратите внимание, что из-за того, что MoonSphere является экранным объектом (основанным на классе Sprite), маску можно применить напрямую к экземпляру MoonSphere с помощью унаследованного свойства mask.


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

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

Ниже показан код для создания фильтра замещения текстуры displaceFilter:

var displaceFilter:DisplacementMapFilter; 
displaceFilter = new DisplacementMapFilter(fisheyeLens, 
                                new Point(radius, 0),  
                                BitmapDataChannel.RED, 
                                BitmapDataChannel.GREEN, 
                                radius, 0);

Первый параметр — fisheyeLens — известен как текстура изображения. В данном случае это объект BitmapData, создаваемый программными средствами. Создание этого изображения описано ниже в разделе Создание растрового изображения путем установки значений пикселов. Остальные параметры указывают, в какой области фильтрованного изображения нужно применить фильтр, какие цветовые каналы использовать для управления эффектом смещения и насколько сильным будет это смещение. Созданный фильтр замещения текстуры применяется к объекту sphere, по-прежнему внутри метода imageLoadComplete():

sphere.filters = [displaceFilter];

Итоговое изображение (с применением маски и фильтра замещения текстуры) показано ниже.


С каждым циклом анимации вращающейся луны содержимое сферы BitmapData заменяется новым снимком исходных данных изображения. Тем не менее, каждый раз применять фильтр заново необязательно, так как фильтр применяется к экземпляру Bitmap (экранному объекту), а не к данным растрового изображения (информации пикселов без сжатия). Помните, что экземпляр Bitmap — это не сами данные растрового изображения, а экранный объект, который выводит данные растрового изображения на экран. Можно провести аналогию: экземпляр Bitmap — это проектор, с помощью которого мы выводим слайды на экран, а объект BitmapData — это собственно слайд, который можно вывести на экран с помощью проектора. Фильтр можно применить напрямую к объекту BitmapData (пользуясь той же аналогией, подрисовать что-нибудь прямо на слайде, чтобы изменить картинку). Фильтр также можно применить к любому экранному объекту, включая экземпляр Bitmap — это можно сравнить с размещением фильтра перед линзой проектора для искажения выводимого на экран изображения (при этом исходный слайд не меняется). Доступ к данным без сжатия осуществляется через свойство bitmapData экземпляра Bitmap, поэтому фильтр можно применить и напрямую к несжатым данным растрового изображения. Тем не менее, лучше применять фильтр к экранному объекту Bitmap, а не к данным растрового изображения.

Подробную информацию об использовании фильтра замещения текстуры в языке ActionScript см. в разделе Фильтрация экранных объектов.

Создание растрового изображения путем установки значений пикселов

Важным свойством фильтра замещения текстуры является то, что он состоит из двух изображений. Одно из них — исходное — это изображение, которое изменяется в результате наложения фильтра. В данном примере исходным изображением является экземпляр Bitmap с именем sphere. Второе изображение, используемое фильтром, — это изображение текстуры. Текстура не отображается на экране. Вместо этого цвет каждого из ее пикселов используется в качестве входа функции замещения, т.е. цвет пиксела с некоторыми координатами x, y в изображении текстуры определяет, какое смещение (физическое изменение положения) применяется к пикселу с такими же координатами х, у в исходном изображении.

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


Так как в примере используется только одно изображение текстуры, оно создается только один раз — в методе imageLoadComplete() (иными словами, после окончания загрузки внешнего изображения). Изображение текстуры с именем fisheyeLens создается в результате вызова метода createFisheyeMap() класса MoonSphere:

var fisheyeLens:BitmapData = createFisheyeMap(radius);

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

private function createFisheyeMap(radius:int):BitmapData 
{ 
    var diameter:int = 2 * radius; 
     
    var result:BitmapData = new BitmapData(diameter, 
                                        diameter, 
                                        false, 
                                        0x808080); 
     
    // Loop through the pixels in the image one by one 
    for (var i:int = 0; i < diameter; i++) 
    { 
        for (var j:int = 0; j < diameter; j++) 
        { 
            // Calculate the x and y distances of this pixel from 
            // the center of the circle (as a percentage of the radius). 
            var pctX:Number = (i - radius) / radius; 
            var pctY:Number = (j - radius) / radius; 
             
            // Calculate the linear distance of this pixel from 
            // the center of the circle (as a percentage of the radius). 
            var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY); 
             
            // If the current pixel is inside the circle, 
            // set its color. 
            if (pctDistance < 1) 
            { 
                // Calculate the appropriate color depending on the 
                // distance of this pixel from the center of the circle. 
                var red:int; 
                var green:int; 
                var blue:int; 
                var rgb:uint; 
                red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY)); 
                green = 0; 
                blue = 0; 
                rgb = (red << 16 | green << 8 | blue); 
                // Set the pixel to the calculated color. 
                result.setPixel(i, j, rgb); 
            } 
        } 
    } 
    return result; 
}

Во-первых, при вызове метода ему присваивается параметр radius, указывающий радиус создаваемого круга. Затем создается объект BitmapData, на котором будет рисоваться круг. Этот объект, названный result, в результате передается назад в качестве возвращаемого методом значения. Как показано в приведенном ниже фрагменте кода, экземпляр result создается с шириной и высотой, равными диаметру круга, является непрозрачным (значение третьего параметра — false) и имеет заливку цвета 0x808080 (серого):

var result:BitmapData = new BitmapData(diameter, 
                                    diameter, 
                                    false, 
                                    0x808080);

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

for (var i:int = 0; i < diameter; i++) 
{ 
    for (var j:int = 0; j < diameter; j++) 
    { 
        ... 
    } 
}

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

  1. Код рассчитывает расстояние от текущего пиксела до центра круга по оси х (i - radius). Это значение нужно разделить на радиус, чтобы получить не абсолютное расстояние, а величину в процентах от радиуса ((i - radius) / radius). Это процентное значение сохраняется в переменной pctX, а аналогичное значение по оси у после вычисления сохраняется в переменной pctY, как показано в приведенном ниже коде.

    var pctX:Number = (i - radius) / radius; 
    var pctY:Number = (j - radius) / radius;
  2. С помощью стандартной тригонометрической формулы и теоремы Пифагора можно рассчитать линейное расстояние от центра круга до текущей точки, зная значения pctX и pctY. Это значение сохраняется в переменной pctDistance:

    var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY);
  3. Затем код проверяет, не превышает ли относительное расстояние единицы (т.е. 100 % радиуса или, иными словами, не лежит ли пиксел за границами круга). Если пиксел оказывается внутри круга, ему присваивается вычисленное значение цвета (здесь это не показано, но описано в шаге 4). Если же он лежит вне круга, то с пикселом ничего не происходит, и его цвет остается серым:

    if (pctDistance < 1) 
    { 
        ... 
    }
  4. Для пикселов, лежащих внутри круга, вычисляется значение цвета. Итоговый цвет — это один из оттенков красного, от черного (0 % красного) в левой части круга до ярко-красного (100 % красного) в правой. Цветовое значение вычисляется с использованием трех компонентов (красного, зеленого и синего):

    red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY)); 
    green = 0; 
    blue = 0;

    Обратите внимание, что какое-либо значение есть только у красного компонента (переменная red). Зеленый и синий компоненты (переменные green и blue) показаны здесь для справки, но на самом деле их можно опустить. Так как целью этого метода является создание круга с красной градиентной заливкой, значения зеленого и синего компонентов не требуются.

    Когда три отдельные величины цветов определены, они сводятся в единое целое значение цвета с помощью стандартного алгоритма битового сдвига:

    rgb = (red << 16 | green << 8 | blue);

    Наконец, когда значение цвета вычислено, оно назначается текущему пикселу с помощью метода setPixel() объекта BitmapData result:

    result.setPixel(i, j, rgb);