Nov 19

Zapewne każdy flash developer ma swój sposób na wczytywanie flash varów. Ja chciałbym przedstawić ten, z którego od jakiegoś czasu korzystam, oparty na wzorcach projektowych Multiton (Multiton Pattern) oraz Strategii.

Rozwiązanie składa się z dwóch klas FlashVars i AbstractFlashVarsProvider oraz interfejsu IFlashVarsProvider.

Klasa FlashVars działa jako Multiton i pozwala tworzyć przy pomocy statycznej metody getInstance(name : String , flashVarsProviderClass : Class) instancje samej siebie, pobierając w argumentach nazwę nowego obiektu oraz nazwę klasy, która dostarczy schemat flash var’ów. FlashVars referencje każdej swojej instancji zapisuje w polu statycznej tablicy __instances, do której dostęp jest możliwy poprzez FlashVars.getInstances() : Array.

Natomiast AbstractFlashVarsProvider jest abstrakcyjną klasą, po której dziedziczą wszystkie klasy, dostarczające nazwy flash var’ów i ich defaultowe wartości. Implementuje ona interfejs IFlashVarsProvider.

Poniżej znajduje się kod źródłowy poszczególnych elementów składowych.

package pl.rafalbociek.utils.flashvars
{
    import flash.display.Stage;
    import flash.utils.getQualifiedClassName;  
   
    /**
     * @author Rafal Bociek 2009
     *
     * Klasa obslugujaca pobieranie flash var'ow.
     *
     * ( Multiton pattern )
     */

     
    public class FlashVars
    {
        /**
         * Wlasciwosci prywatne
         */

       
        // Referencja do obiektu, ktory dostarcza konfiguracje flash var'ow
       
        private var __flashVarsProvider : AbstractFlashVarsProvider;
       
       
       
        // Nazwa/klucz instancji multiton'a
       
        private var __name              : String;                              
       
       
       
        // Enforcer blokujacy wywolywanie konstruktora spoza tego pakietu
       
        private var __sE                : $MultitonEnforcer;                     
       
       
       
        /**
         * Statyczne wlasciwosci prywatne
         */

       
        // Tablica instancji multiton'a
       
        private static var __instances  : Array = new Array();                 


       
        /**
         * Funkcja tworzy nowy obiekt FlashVars.
         * Konstruktor moze byc wywolany tylko poprzez getInstance()
         * @param name Nazwa/klucz tworzonej instancji
         * @param flashVarsProviderClass Klasa provider'a flash var'ow
         * @param mE Referencja do enforcer'a
         */

         
        public function FlashVars(name : String , flashVarsProviderClass : Class,
                                                mE : $MultitonEnforcer) : void
        {
            __flashVarsProvider = new flashVarsProviderClass();
           
            __sE    = mE;
            __name  = name;
        }



        /**
         * Funkcja zwraca instancje multiton'a o okreslonym kluczu.
         * @param key Nazwa instancji multiton'a.
         * @param flashVarsProviderClass Klasa, ktora dostarcza
         * konfiguracje flash var'ow.
         */

         
        public static function getInstance ( key : String ,
                                    flashVarsProviderClass : Class ) : FlashVars
        {
            var instance : FlashVars = __instances[key];
           
            if(instance == null)
            {
                instance = new FlashVars( key , flashVarsProviderClass ,
                                            new $MultitonEnforcer() );
               
                __instances[key] = instance;
            }
           
            return instance;
        }


       
        /**
         * Funkcja pobiera flash var'y z okreslonego stage'a.
         */

       
        public function loadVariables(stage : Stage) : void
        {
            var instance : FlashVars = __instances[__name];
           
            if(instance == null){}
            else
            {
                __flashVarsProvider.loadFlashVars(stage);
            }
        }



        /**
         * Wyswietla wszystkie flash var'y, przechowywane w danej instancji;
         */

         
        public function describeFlashVars() : String
        {
            var str : String = '';
            str += 'n| FLASH VARS: ';
            str += 'n|';
           
            for (var fvName : String in __flashVarsProvider.getFlashVars())
            {
                str += 'n| NAME: ' + fvName + ' VALUE: ' +
                __flashVarsProvider.getFlashVars()[fvName];
            }

            str += 'n|n';
           
            return str;
        }



        /**
         * Funkcja zwraca asocjacyjna tablice z obiektu,
         * ktory dostarcza flash var'y.
         */

         
        public function getData() : Array
        {          
            return (__flashVarsProvider != null)?
            __flashVarsProvider.getFlashVars() : null;
        }


       
        /**
         * Funkcja zwraca tablice instancji multiton'a.
         */

         
        public static function getInstances () : Array
        {
            return __instances;
        }


       
        /**
         * Funkcja zwraca nazwe konkretnej instancji multiton'a.
         */

         
        public function getName () : String
        {
            return __name;
        }


       
        /**
         * Funkcja zwraca ciag znakow, opisujacy konkretna instancje multiton'a.
         */

         
        public function toString () : String
        {
            return '| FLASVARS MULTITON | [ name: ' + getName() +
            ' | provider: ' + getQualifiedClassName(__flashVarsProvider) + ' ]';
        }
    }
}
internal class $MultitonEnforcer
{
    public function $MultitonEnforcer()
    {
       
    }
}

FlashVars.as

package pl.rafalbociek.utils.flashvars.interfaces
{
    import flash.display.Stage;    

    /**
     * @author Rafal Bociek 2009
     *
     * Interfejs klas,
     * dostarczajacych schematy flash var'ow.
     */

     
    public interface IFlashVarsProvider
    {
        function loadFlashVars(stage : Stage)   : void;
        function getFlashVars()                 : Array;  
    }
}

IFlashVarsProvider.as

package pl.rafalbociek.utils.flashvars
{
    import pl.rafalbociek.utils.Output;
    import pl.rafalbociek.utils.flashvars.interfaces.IFlashVarsProvider;
   
    import flash.display.Stage;    

    /**
     * @author Rafal Bociek 2009
     *
     * Klasa abstrakcyjna, po ktorej dziedzicza wszystkie klasy,
     * dostarczajace konfiguracje flash var'ow.
     */

    public class AbstractFlashVarsProvider implements IFlashVarsProvider
    {
        /**
         * Tablica asocjacyjna klucz-wartosc.
         * Kluczem jest nazwa flas var'a a wartoscia dane flash var'a.
         */

        protected var _flashVars    : Array;
       
        public function AbstractFlashVarsProvider ()
        {
            /**
             * Konstruktor powinien byc nadpisany w klasie
             * dziedzicacej po AbstractFlashVarsProvider.
             *
             * W konstruktorze powinny byc zainicjowane default'owe
             * wartosci flash varow, np.
             *
             * _flashVars = new Array();
             *
             * _flashVars[FV_1] = 'default value of flash var 1';
             * _flashVars[FV_2] = 'default value of flash var 2';
             * _flashVars[FV_3] = 'default value of flash var 3';
             */

        }

        public function loadFlashVars ( stage : Stage ) : void
        {
            if(_flashVars != null)
            {
                if( stage.loaderInfo.parameters != null )
                {
                    try
                    {
                        for (var fvName : String in _flashVars)
                        {
                            _flashVars[fvName] =
                            (stage.loaderInfo.parameters[fvName] != null) ?
                            stage.loaderInfo.parameters[fvName] :
                            _flashVars[fvName];
                        }
                    }
                    catch ( e : Error )
                    {
                        Output.println('[ Wystapil blad przy probie'+
                        ' wczytania flash var\'ow! ]');
                       
                        Output.println('-----------------------------'+
                        '------------------------');
                       
                        Output.println( e.message );
                    }
                }
            }
        }

        public function getFlashVars () : Array
        {
            return _flashVars;
        }
    }
}

AbstractFlashVarsProvider.as

Jak skorzystać z tego sposobu wczytywania? Bardzo prosto…
Tworzę przykładową klasę SampleFlashVarsProvider1. Dziedziczy ona po AbstractFlashVarsProvider i przejmuje mechanizmy pobierania flash var’ów i pracy z nimi. Moim zadaniem jest tylko zdefiniowanie nazw wczytywanych zmiennych, ich początkowych wartości oraz nazwy ‘dostawcy’ danych (PROVIDER_NAME).

package pl.rafalbociek.utils.flashvars
{
    /**
     * @author Rafal Bociek 2009
     *
     * Klasa przechowujaca nazwy flash var'ow
     * oraz ich default'owe wartosci.
     */

     
    public class SampleFlashVarsProvider1 extends AbstractFlashVarsProvider
    {
        public static const PROVIDER_NAME : String = 'sampleFlashVarsProvider1';
       
        /**
         * Nazwy flash var'ow
         */

         
        public static const SAMPLE_FLASHVAR_1   : String = 'sampleFlashVar1';
        public static const SAMPLE_FLASHVAR_2   : String = 'sampleFlashVar2';
        public static const SAMPLE_FLASHVAR_3   : String = 'sampleFlashVar3';
        public static const SAMPLE_FLASHVAR_4   : String = 'sampleFlashVar4';
        public static const SAMPLE_FLASHVAR_5   : String = 'sampleFlashVar5';
       
        /**
         * Konstruktor
         */

       
        public function SampleFlashVarsProvider1 ()
        {
            /**
             * Default'owe flash var'y
             */

           
            _flashVars = new Array();
           
            _flashVars[SAMPLE_FLASHVAR_1] = 'defaultValueOfSampleFlashVar1';
            _flashVars[SAMPLE_FLASHVAR_2] = 'defaultValueOfSampleFlashVar2';
            _flashVars[SAMPLE_FLASHVAR_3] = 'defaultValueOfSampleFlashVar3';
            _flashVars[SAMPLE_FLASHVAR_4] = 'defaultValueOfSampleFlashVar4';
            _flashVars[SAMPLE_FLASHVAR_5] = 'defaultValueOfSampleFlashVar5';
        }
    }
}

SampleFlashVarsProvider1.as

Pozostało już tylko przetestowanie mechanizmu.

package pl.rafalbociek.utils.flashvars
{
    import pl.rafalbociek.utils.Output;
    import flash.display.Sprite;
   
    /**
     * @author Rafal Bociek 2009
     *
     * Klasa pokazujaca sposob korzystania z klasy FlashVars.
     *
     * ( The example of using FlashVars class. )
     */

     
    public class FlashVarsTestDC extends Sprite
    {
       
        /**
         * Konstruktor
         */

       
        public function FlashVarsTestDC ()
        {          
            Output.debugEnabled = true;
           
            // Tworzę instancję FlashVars o nazwie
            // sampleFlashVarsProvider1
           
            var fvCfg1 : FlashVars = FlashVars.getInstance  (
                                        SampleFlashVarsProvider1.PROVIDER_NAME ,
                                        SampleFlashVarsProvider1
                                    );
           
            // Wczytuje dane flash var'ow
                                           
            fvCfg1.loadVariables(stage);
           
           
            // Wyswietlam zawartosc wczytanych flash var'ow
           
            Output.println( fvCfg1.describeFlashVars( ) );
           
           
            // Pobieram testowo wartosc flash var'a
            // SampleFlashVarsProvider1.SAMPLE_FLASHVAR_3
           
            Output.println( SampleFlashVarsProvider1.SAMPLE_FLASHVAR_3 +
            ' = '+fvCfg1.getData()[SampleFlashVarsProvider1.SAMPLE_FLASHVAR_3]);
        }
    }
}

FlashVarsTestDC.as

Mimo, że na pierwszy rzut oka duża ilość kodu może odstraszyć, dużą zaletą jest łatwość tworzenia różnych konfiguracji flash varów poprzez dziedziczenie po AbstractFlashVarsProvider oraz dostęp do wczytanych danych statycznymi metodami klasy FlashVars. Klasa FlashVars pilnuje także by nie tworzone były zbędne referencje do jej instancji.

Oczywiście w przypadku wczytywania jednego flash var’a nie ma sensu tego typu rozwiązanie, ale w większych projektach sprawdza się bardzo dobrze i je polecam.

Źródła do pobrania stąd.

Nov 14

waveform

Dziś pokażę w jaki sposób bez udziału zewnętrznych aplikacji, wyświetlić wykres fali dźwiękowej pliku WAV we Flash Playerze 9. W tym celu napisałem dwie klasy WaveSound oraz WaveExtractor. Pierwsza z nich jest wzorcem obiektu, który odzwierciedla plik WAV w Action Script 3.0. Druga natomiast to klasa statyczna, która tworzy obiekt WaveSound w oparciu o dane w postaci tablicy bajtów.

Klasy napisałem, opierając się na specyfikacji formatu WAV oraz źródłach biblioteki popforge, stworzonej przez Andre Michelle oraz Joa Ebert’a.  Nie wykorzystałem gotowego rozwiązania, ponieważ wersja przedstawiona przez Andre i Joa jest wolniejsza od mojej o kilkadziesiąt procent, co jest bardzo ważne w przypadku przetwarzania tak dużej ilości danych.

W przypadku dźwięku stereofonicznego zapisanego w jakości CD jako PCM (Pulse Code Modulation), o próbkowaniu 44100 bitów na sekundę, dwu kanałach (dźwięk stereo), 60 sekundowy utwór zawiera 44100 * 60 sampli. Dla przykładowego pliku audio, którego długość wynosi około 4 minuty daje to ponad 10 000 000 sampli!

Pisząc sampel mam na myśli obiekt, który ma dwie właściwości: amplitudy kanału lewego oraz kanału prawego dźwięku w danym momencie czasu. Wartości każdego kanału to liczby rzeczywiste z zakresu [-1,1]. Dokładniej, są to wszystkie liczby, które można utworzyć, dzieląc liczby z przedziału od -32768  do 32767 (czyli liczby całkowite typu short – zapisane na dwóch bajtach) przez liczbę 32767: { -32768/ 32767 , -32767/ 32767, -32766/ 32767, … , 0/ 32767, 1/32767, 2/32767, … , 32767/ 32767 }.

Aby narysować wykres amplitudy od czasu dla pliku WAV wykonuję trzy kroki:

1) wczytuję plik WAV jako tablicę bajtów,

2) konwertuję ciąg bajtów na obiekt typu WaveSound przy pomocy metody extract() klasy WaveExtractor,

3) rysuję wykres w oparciu o tablicę sampli obiektu utworzonego w poprzednim kroku.

Poniżej zamieszczam kod źródłowy 4 klas: WaveFormDrawerDC, WaveExtractor, WaveSound oraz Output.

Klasę Output wykorzystuję wyłącznie jako zamiennik standardowej metody trace(), natomiast WaveFormDrawerDC, to Document Class tego przykładu. To tu znajduje się kod rysujący wykres i parametry konfiguracyjne.

WaveSound.as

package pl.rafalbociek.utils.waveform
{
    /**
     * Imports
     */

   
    import flash.utils.ByteArray;      

    /**
     * @author Rafal Bociek 2009
     *
     * <br><br>Klasa, bedaca odzwierciedleniem pliku wav
     * po odkodowaniu z tablicy bajtow. <br>Przechowuje
     * informacje o ilosci kanalow, predkosci
     * probkowania,<br>tablice sampli dzwieku oraz kilka innych
     * wlasciwosci.<br><br>
     *
     * Tablica sampli budowana jest w nastepujacy sposob:<br><br>
     *
     * 1) dla dzwieku stereofonicznego<br><br>
     *
     * <p style="padding-left:50px">samples = [l,r,l,r,l,r,l,r]</p><br>
     *
     * 2) dla dzwieku monofonicznego <br><br>
     *
     * <p style="padding-left:50px">samples = [l,l,l,l,l]</p><br>
     *
     * , gdzie l - wartosc amplitudy dzwieku kanalu lewego,<br>r -  wartosc
     * amplitudy dzwieku kanalu prawego. <br>Wartosci l,r sa liczbami
     * rzeczywistymi z przedzialu [-1,-1].<br><br>
     *
     */

     
    public class WaveSound
    {
        public static const MONO        : int = 1;
        public static const STEREO      : int = 2;
       
        public static const BITS_8      : int = 8;
        public static const BITS_16     : int = 16;
       
        public static const CHUNK_FMT   : String = 'fmt ';
        public static const CHUNK_DATA  : String = 'data';
               
        private var __compression       : uint;
        private var __channels          : uint;
        private var __rate              : uint;
       
        private var __bps               : uint;
        private var __blockAlign        : uint;
        private var __bits              : uint;
       
        private var __samples           : Array;
        private var __samplesNum        : uint;
       
        private var __data              : ByteArray;
        private var __dataLen           : uint;
       
        static public function decode( bytes : ByteArray ) : WaveSound
        {
            return WaveExtractor.extract( bytes );
        }
       
        /**
         * Konstruktor
         */

         
        public function WaveSound ()
        {
           
        }
         
        public function toString() : String
        {
            var str : String = '';
               
                str += 'nnWaveSound parameters:nn';
                str += 'COMPRESSION: tt' + __compression + 'n';
                str += 'CHANNELS: ttt' + __channels + 'n';
                str += 'RATE: tttt' + __rate + 'n';
                str += 'BITS PER SECOND: t' + __bps + 'n';
                str += 'SAMPLES NUMBER: t' + __samplesNum + 'n';
                str += 'n';
           
           
            return str;
        }  
       
       

        /**
         * Getter'y i setter'y
         */

       
        public function get compression() : uint
        {
            return __compression;
        }
       
        public function set compression(_audioCompression : uint) : void
        {
            __compression = _audioCompression;
        }
       
        public function get channels() : uint
        {
            return __channels;
        }
       
        public function set channels(_audioChannels : uint) : void
        {
            __channels = _audioChannels;
        }
       
        public function get rate() : uint
        {
            return __rate;
        }
       
        public function set rate(_audioRate : uint) : void
        {
            __rate = _audioRate;
        }
       
        public function get bps() : uint
        {
            return __bps;
        }
       
        public function set bps(_audioBPS : uint) : void
        {
            __bps = _audioBPS;
        }
       
        public function get samples() : Array
        {
            return __samples;
        }
       
        public function set samples(_audioSamples : Array) : void
        {
            __samples = _audioSamples;
        }
       
        public function get samplesNum() : uint
        {
            return __samplesNum;
        }
       
        public function set samplesNum(_audioSamplesNum : uint) : void
        {
            __samplesNum = _audioSamplesNum;
        }
       
        public function get data() : ByteArray
        {
            return __data;
        }
       
        public function set data(_audioData : ByteArray) : void
        {
            __data = _audioData;
        }
       
        public function get bits() : uint
        {
            return __bits;
        }
       
        public function set bits(_audioBits : uint) : void
        {
            __bits = _audioBits;
        }
       
        public function get blockAlign() : uint
        {
            return __blockAlign;
        }
       
        public function set blockAlign(_audioBlockAlign : uint) : void
        {
            __blockAlign = _audioBlockAlign;
        }
       
        public function get dataLen () : uint
        {
            return __dataLen;
        }
       
        public function set dataLen (_audioDataLen : uint) : void
        {
            __dataLen = _audioDataLen;
        }
    }
}

WaveExtractor.as

package pl.rafalbociek.utils.waveform
{
    /**
     * Imports
     */

   
    import flash.utils.ByteArray;
    import flash.utils.Endian;   
   
    /**
     *
     * @author Rafal Bociek 2009<br><br>
     *
     * Klasa statyczna, sluzaca do dekodowania plikow wav i wyciagania z nich
     * <br>tzw. sampli. Kazdy sampel posiada informacje o amplitudzie dzwieku
     * w danym<br>momencie czasu dla kanalow lewego i prawego (dla dzwieku
     * stereofonicznego)<br>oraz 'centralnego' dla sciezki monofonicznej.
     *
     * <br><br>Przyklad uzycia znajduje sie w klasie WaveFormDrawerDC.
     *
     */

   
    public class WaveExtractor
    {
       
       
       
        /**
         * Funkcja dekoduje tablice bajtow podana w argumencie i zwraca<br>
         * obiekt WaveSound.
         *
         * @param audioBA Tablica bajtow dzwieku wav.
         * @return Obiekt typu WaveSound.
         */

         
        static public function extract ( bytes : ByteArray ) : WaveSound
        {
            var audio : WaveSound = new WaveSound();
           
                bytes.position = 0;
                bytes.endian = Endian.LITTLE_ENDIAN;
               
                bytes.readUTFBytes( 4 );
                bytes.readUnsignedInt();
                bytes.readUTFBytes( 4 );
           
                var len     : uint;
                var pos     : uint;
                var chunkId : String;
               
                var data    : ByteArray = new ByteArray();
               
                chunkId = bytes.readUTFBytes( 4 );
                len     = bytes.readUnsignedInt();
                pos     = bytes.position;
               
                audio.compression   = bytes.readUnsignedShort();
                audio.channels      = bytes.readUnsignedShort();
                audio.rate          = bytes.readUnsignedInt();
                audio.bps           = bytes.readUnsignedInt();
                audio.blockAlign    = bytes.readUnsignedShort();
                audio.bits          = bytes.readUnsignedShort();
               
                while( bytes.position < bytes.length )
                {
                    chunkId = bytes.readUTFBytes( 4 );
                    len     = bytes.readUnsignedInt();
                    pos     = bytes.position;
       
                    if( chunkId == WaveSound.CHUNK_DATA)
                    {      
                        data            = new ByteArray(); 
                        data.endian     = Endian.LITTLE_ENDIAN;
                               
                        data.writeBytes ( bytes , pos , len );
                        data.position   = 0;                       
                           
                        audio.data      = data;
                           
                        bytes.position  = pos + len;
                           
                        audio.dataLen   = len;
                    }
                    else
                    {
                        bytes.position  = pos + len;
                    }
                   
                }
               
                audio.samplesNum = data.length;
               
                if( audio.channels == WaveSound.STEREO )    
                {
                    audio.samplesNum = ( audio.samplesNum >> 1 );
                }
                if( audio.bits == WaveSound.BITS_16 )
                {
                    audio.samplesNum = ( audio.samplesNum >> 1 );
                }
               
                audio.samples = $createSamplesArray ( audio );
           
            return audio;
        }
       
       
       
        /**
         * Funkcja odczytuje z obiektu WaveSound kolejne
         * wartosci<br>sampli i zapisuje je w tablicy, ktora zwraca<br>
         * po zakonczeniu dzialania.
         *
         * @param wav Obiekt typu WaveSound.
         * @return Tablica sampli dzwieku podanego w argumencie.
         */

         
        static internal function $createSamplesArray ( wav : WaveSound ) : Array
        {
            var data        : ByteArray = wav.data;
            var sampleCount : uint      = wav.samplesNum;
            var channels    : uint      = wav.channels;
            var bits        : uint      = wav.bits;

            var i           : int       = 0;
           
            var samples     : Array     = new Array();
           
            if( channels == WaveSound.STEREO )
            {              
                if( bits == WaveSound.BITS_16 )
                {
                    for( i = 0 ; i < sampleCount ; i++ )
                    {                                              
                        samples.push ( data.readShort() / 0x7fff );
                        samples.push ( data.readShort() / 0x7fff );
                    }
                }
                else
                {
                    for( i = 0 ; i < sampleCount ; i++ )
                    {
                        samples.push ( data.readUnsignedByte() / 0x80 - 1 );
                        samples.push ( data.readUnsignedByte() / 0x80 - 1 );
                    }
                }
            }
            else if( channels == WaveSound.MONO )
            {
                if( bits == WaveSound.BITS_16 )
                {
                    for( i = 0 ; i < sampleCount ; i++ )
                    {
                        samples.push ( data.readShort() / 0x7fff );
                    }
                }
                else
                {
                    for( i = 0 ; i < sampleCount ; i++ )
                    {
                        samples.push ( data.readUnsignedByte() / 0x80 - 1 );
                    }
                }
            }
           
            return samples;
        }
    }
}

WaveFormDrawerDC.as

package pl.rafalbociek.utils.waveform
{
    /**
     * Imports
     */

     
    import pl.rafalbociek.utils.Output;
   
    import flash.display.Graphics;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;
    import flash.utils.ByteArray;  
   
    /**
     * @author Rafal Bociek 2009
     *
     * <br><br>Document Class aplikacji rysujacej wykres
     * amplitudy dzwieku <br>w zaleznosci od czasu dla pliku audio
     * zapisanego jako WAV.
     *
     */

     
    public class WaveFormDrawerDC extends Sprite
    {
        /**
         * PARAMETRY WYKRESU / GRAPH PARAMETERS
         *
         *
         * G_W      - szerokosc wykresu                     (GRAPH WIDTH)
         * G_H      - wysokosc wykresu                      (GRAPH HEIGHT)
         *
         * G_LT     - grubosc linii wykresu                 (GRAPH LINE THICKNESS)
         * G_LC     - kolor linii wykresu                   (GRAPH LINE COLOR)
         * G_LA     - przezroczystosc linii wykresu         (GRAPH LINE ALPHA)
         *
         * GD_LT    - grubosc linii siatki                  (GRID LINE THICKNESS)
         * GD_LC    - kolor linii siatki                    (GRID LINE COLOR)
         * GD_LA    - przezroczystosc linii siatki          (GRID LINE ALPHA)
         * GD_BGC   - kolor tla siatki                      (GRID BACKGROUND COLOR)
         * GD_BGA   - przezroczystosc tla siatki            (GRID BACKGROUND ALPHA)
         *
         * G_CC     - kolor punktu na wykresie              (GRAPH CIRCLE COLOR)
         * G_CA     - przezroczystosc punktu na wykresie    (GRAPH CIRCLE ALPHA)
         *
         * GD_XD    - liczba linii pionowych siatki         (GRID X-AXIS DIVISION)
         * GD_YD    - liczba linii poziomych siatki         (GRID Y-AXIS DIVISION)
         *
         * F_URL    - adres URL do pliku .wav               (WAVE FILE URL)
         *
         */

         
        public static const G_W     : int       = 600;
        public static const G_H     : int       = 80;
       
        public static const G_LT    : int       = 1;
        public static const G_LC    : int       = 0x9c9c9c;
        public static const G_LA    : Number    = 1.0;
       
        public static const GD_LT   : int       = 1;
        public static const GD_LC   : uint      = 0xf0f1f6;
        public static const GD_LA   : Number    = 1.0;
        public static const GD_BGC  : uint      = 0xfdfdfd;
        public static const GD_BGA  : Number    = 1.0;
       
        public static const G_CC    : int       = 0xcc0033;
        public static const G_CA    : Number    = 1.0;
       
        public static const GD_XD   : int       = 300;  // 5*60
        public static const GD_YD   : int       = 80;   // 5*16;
       
        public static const F_URL   : String    = 'embed/sampleAudio.wav';
       
        /**
         * Wlasciwosci prywatne
         */

         
        private var __waveSound     : WaveSound = null;
       
        /**
         * Konstruktor
         */

       
        public function WaveFormDrawerDC()
        {
            /**
             * Przy pomocy URLLoader'a wczytuje plik wav jako tablice bajtow.
             */

           
            Output.debugEnabled = true;
           
            var uL : URLLoader = new URLLoader();
           
                uL.dataFormat = URLLoaderDataFormat.BINARY;
               
                uL.addEventListener(Event.COMPLETE                      , __completeEH);
                uL.addEventListener(IOErrorEvent.IO_ERROR               , __ioeEH);
                uL.addEventListener(SecurityErrorEvent.SECURITY_ERROR   , __seEH);
               
            uL.load( new URLRequest( F_URL ) );
        }
       
        /**
         * Event handler wywolywany w przypadku
         * wystapienia bledu bezpieczenstwa
         * przy zadaniu wczytania pliku.
         */

       
        private function __seEH (event : SecurityErrorEvent) : void
        {
            Output.println('Wystapil blad bezpieczenstwa!');
        }

        /**
         * Event handler wywolywany w przypadku
         * wystapienia bledu wejscia/wyjscia.
         */


        private function __ioeEH (event : IOErrorEvent) : void
        {
            Output.println('Wystapil blad wejscia!');
        }



        /**
         * Event handler wywolywany po zakonczeniu operacji wczytania pliku.
         */

         
        private function __completeEH(event : Event) : void
        {
            Output.println('Plik wczytany poprawnie.');
                       
            __drawGraph((event.target as URLLoader).data);
        }


       
        /**
         * Metoda rysujaca wykres i dodajaca go na scene.
         */

         
        private function __drawGraph(byteArray : ByteArray) : void
        {          
            /**
             * Dekoduje tablice bajtow do AudioWaveFormat.
             */

           
            var start   : Number    = (new Date()).getTime();
               
            __waveSound         = WaveExtractor.extract(byteArray);
           
            Output.println(__waveSound.toString());
           
            /**
             * Tworze referencje do tablicy sampli wczytanego pliku.
             */

           
            var sRef : Array    = __waveSound.samples;
           
            /**
             * Tworze dwa kontenery typu Sprite dla kanalow lewego i prawego
             * oraz definiuje referencje do ich parametrow graphics.
             */

           
            var  leftChannel    : Sprite = new Sprite();
            var rightChannel    : Sprite = new Sprite();
           
            var lGfx            : Graphics =  leftChannel.graphics;
            var rGfx            : Graphics = rightChannel.graphics;
           
            /**
             * Rysuje siatke wykresu.
             */

           
            __drawGrid( lGfx , G_W , G_H * 2 , GD_XD , GD_YD );
            __drawGrid( rGfx , G_W , G_H * 2 , GD_XD , GD_YD );
           
            /**
             * Ustawiam lewy i prawy zakres czasu, dla ktorego kresle wykres.
             */

           
            var left    : uint  = 2 * __waveSound.rate * 2;
            var right   : uint  = 2 * __waveSound.rate * 3;
           
            /**
             * Definiuje zmienne potrzebne przy rysowaniu.
             */

           
            var i       : int   = 0;
            var step    : int   = 0;
           
            var range   : int   = right - left;
           
            /**
             * W zaleznosci od ilosci kanalow utworu
             * (STEREO lub MONO) rysuje wykres fali dzwiekowej.
             */

           
            if(__waveSound.channels == WaveSound.STEREO)
            {
                /**
                 * Obliczam krok iteracji.
                 *
                 * Jezeli ilosc sampli, dla ktorych rysuje wykres jest mniejsza
                 * od (GRAPH_WIDTH * 2 * 100), to krok = 2. W innym wypadku
                 * krok = 2 * Math.ceil(range / GRAPH_WIDTH / 2 / 100).
                 */

                 
                step = (range <= G_W * 2 * 100)? 2 : 2 * Math.ceil(range/G_W/2/100);
               
                /**
                 * Kresle wykres
                 */

               
                lGfx.moveTo(0 , G_H * sRef[0]);
                rGfx.moveTo(0 , G_H * sRef[1]);
               
                lGfx.lineStyle(0,0,0);
                rGfx.lineStyle(0,0,0);
               
                /**
                 * Wykres dla fragmentu utworu o ilosci sampli mniejszej od
                 * 2 * G_W rysuje z wieksza dokladnoscia - stad ponizszy warunek.
                 */

               
                if(range <= G_W * 2)
                {                  
                    for ( i = left + 1 ; i <= right ; i += step)
                    {
                        lGfx.lineStyle( G_LT , G_LC , G_LA );
                        rGfx.lineStyle( G_LT , G_LC , G_LA );
                       
                        lGfx.lineTo((i - left) / range * G_W , - G_H * sRef[i-1]);
                        rGfx.lineTo((i - left) / range * G_W , - G_H * sRef[i]);
                       
                        lGfx.lineStyle(0,0,0);
                        rGfx.lineStyle(0,0,0);
                                   
                        lGfx.beginFill(G_CC , G_CA);
                        rGfx.beginFill(G_CC , G_CA);
                                   
                        lGfx.drawCircle(
                                            (i - left) / range * G_W,
                                            - G_H * sRef[i-1],
                                            G_LT*2
                                        );
                                   
                        rGfx.drawCircle(
                                            (i - left) / range * G_W,
                                            - G_H * sRef[i],
                                            G_LT*2
                                        );
                                       
                        lGfx.endFill();            
                        rGfx.endFill();                    
                    }
                   
                    lGfx.beginFill(G_CC , G_CA);
                    rGfx.beginFill(G_CC , G_CA);
                   
                    lGfx.drawCircle(0 - G_LT , G_H * sRef[0] - 1,G_LT * 2 );
                    rGfx.drawCircle(0 - G_LT , G_H * sRef[1] - 1,G_LT * 2 );
                   
                    lGfx.endFill();
                    rGfx.endFill();
                }
                else
                {
                    lGfx.endFill();
                    rGfx.endFill();
                   
                    lGfx.lineStyle( G_LT , G_CC , G_LA );
                    rGfx.lineStyle( G_LT , G_CC , G_LA );
                   
                    for ( i = left + 1; i <= right ; i += step)
                    {
                         lGfx.lineTo(
                                        (i - left) / range * G_W ,
                                        - G_H * sRef[i-1]
                                    );
                                   
                          rGfx.lineTo(
                                        (i - left) / range * G_W ,
                                        - G_H * sRef[i]
                                    );         
                    }
                }
            }
            else if(__waveSound.channels == WaveSound.MONO)
            {
                /**
                 * Wersja dla dzwieku MONO.
                 */

               
                step = (range <= G_W * 1 * 50) ? 1 : range / G_W / 100;
               
                lGfx.moveTo(0 , G_W * sRef[0]);
                rGfx.moveTo(0 , G_W * sRef[0]);
               
                for ( i = left + 1 ; i <= right ; i += step)
                {
                    lGfx.lineTo (
                                    (i - left) / range * G_W ,
                                    G_W * -1 / sRef[i]
                                );
                   
                    rGfx.lineTo (
                                    (i - left) / range * G_W ,
                                    G_W * -1 / sRef[i]
                                );
                }
            }
           
            /**
             * Pozycjonuje i dodaje na display liste
             * wykresy kanalow lewego i prawego.
             */

           
             leftChannel.x = 20;
            rightChannel.x = 20;
           
             leftChannel.y = 100;
            rightChannel.y = 300;
           
            addChild( leftChannel );
            addChild( rightChannel );
           
            /**
             * Wyswietlam czas trwania operacji
             */

           
            Output.println('Duration: ' + ((new Date()).getTime() - start));
        }


       
        /**
         * Metoda rysujaca siatke pod wykres fali dzwiekowej.<br>
         * @param gfx Referencja do obiektu typu Graphics,
         * na ktorym powstanie siatka.
         * @param width Szerokosc wykresu.
         * @param height Wysokosc wykresu * 2.
         * @param xSteps Ilosc krokow podzialu osi OX.
         * @param ySteps Ilosc krokow podzialu osi OY.
         */

         
        private function __drawGrid (
                                        gfx     : Graphics ,
                                        width   : int = 600,
                                        height  : int = 80,
                                        xSteps  : int = 0,
                                        ySteps  : int = 10
                                   
                                    ) : void
        {
            gfx.beginFill( GD_BGC , GD_BGA);
            gfx.drawRect(0, -height/2, width, height);
            gfx.endFill();
           
            var i : int;
           
            gfx.lineStyle( GD_LT , GD_LC , GD_LA );
           
            for (i = 0 ; i <= ySteps ; i++)
            {
                gfx.moveTo(0        , i * height / ySteps - height/2);
                gfx.lineTo(width    , i * height / ySteps - height/2);
            }
           
            for (i = 0 ; i <= xSteps ; i++)
            {
                gfx.moveTo(i * width / xSteps , - height/2);
                gfx.lineTo(i * width / xSteps , height/2);
            }
           
            gfx.lineStyle( GD_LT , GD_LC - 10000 , GD_LA );
           
            gfx.moveTo(0 , 0);
            gfx.lineTo(width , 0);
        }
    }
}

Output.as

package pl.rafalbociek.utils
{
    /**
     * Imports
     */

     
    import flash.external.ExternalInterface;   
   
    /**
     * @author Rafal Bociek 2009
     *
     * <br><br>Klasa zastepujaca standardowa metode trace().
     * <br>Dzieki niej mozliwe jest proste debug'owanie w konsoli FireBug'a.
     *
     */

     
    public class Output
    {
        /**
         * Flaga, oznaczajaca czy funkcja debugowania jest aktywna.
         */

         
        private static var __debugEnabled : Boolean;
       
       
       
        /**
         * Getter'y i setter'y
         */

       
        public static function set debugEnabled(value : Boolean) : void
        {
            __debugEnabled = value;
        }
       
        public static function get debugEnabled() : Boolean
        {
            return __debugEnabled;
        }
       
        /**
         * Metoda wyswietla w oknie Output oraz w konsoli<br>Firebug'a
         * kolejne argumenty funkcji.
         * @param args Lista wartosci do wyrzucenia na konsole.
         */

        public static function print(...args) : void
        {
            if(__debugEnabled)
            {
                for (var i : int = 0; i < args.length; i++)
                {
                    trace(args[i]);
                   
                    if(ExternalInterface.available)
                    {
                        ExternalInterface.call('console.log' , args[i]);
                    }
                }
            }
        }
       
        /**
         * Metoda wyswietla w oknie Output oraz w konsoli<br>Firebug'a
         * elementy tablicy.
         * @param array Tablica do wyrzucenia na konsole.
         */

        public static function printArray(array : Array) : void
        {
            if(__debugEnabled)
            {
                var str : String;
               
                if(array.length > 0)
                {
                    for (var i : int = 0; i < array.length; i++)
                    {
                        str = 'INDEX: ' + i + ' | ' + 'VALUE: ' + array[i];
                       
                        trace(str);
                       
                        if(ExternalInterface.available)
                        {
                            ExternalInterface.call( 'console.log' , str );
                        }
                    }
                }
                else
                {  
                    for (var key : String in array)
                    {
                        str = 'KEY: ' + key + ' | ' + 'VALUE: ' + array[key];
                       
                        trace(str);
                       
                        if(ExternalInterface.available)
                        {
                            ExternalInterface.call( 'console.log' , str );
                        }
                    }
                }
            }
        }



        /**
         * Metoda wyswietla ciag znakow str w oknie Output oraz konsoli<br>
         * Firebug'a.
         * @param str Ciag znakow do wyswietlenia.
         */

        public static function println(str : String) : void
        {
            if(__debugEnabled)
            {
                trace(str);
                   
                if(ExternalInterface.available)
                {
                    ExternalInterface.call('console.log', str);
                }
            }
        }
    }
}

Przed uruchomieniem trzeba zmienić ścieżkę do pliku w klasie WaveFormDrawerDC w linijce public static const F_URL : String = ‘embed/sampleAudio.wav’; tak by wskazywał plik WAV, którego wykres będzie rysowany. Ze względu na wielkość plików WAV i praw do nich, nie zamieszczam ich na serwerze.

Kody źródłowe oraz pliki fla i swf do pobrania stąd.

Nov 05

To już ostatnia część mojej wypowiedzi na temat krzywych Béziera. Pokażę sposób wyliczania położenia punktów na krzywych za pomocą algorytmu de Casteljau i jego implementację w AS 3.0.

Przyjmijmy, że naszą krzywą określa n+1 punktów P0, P1, P2 … Pn. Każdy z odcinków |P0P1| , |P1P2| , … , |Pn-1Pn| dzielimy w stosunku t : (1-t), otrzymując n punktów A0, A1, A2, … , An-1. Po raz kolejny odcinki |A0A1| , |A1A2| , … , |An-2An-1| dzielimy w stosunku t : (1-t), wyznaczając n – 1 punktów B0, B1, B2, … , Bn-2. Operację powtarzamy do momentu, w którym ilość wygenerowanych punktów jest równa 1. W ten sposób otrzymujemy punkt krzywej dla parametru t.

Jeśli wykonamy tę operację dla wszystkich t z przedziału <1 , 0> wyznaczymy wszystkie punkty krzywej Béziera.

Działanie algorytmu, który wyznacza punkt dla t = 0.5 krzywej określonej punktami P0, P1, P2, P3, P4 ilustruje rysunek
deCasteljau

Implementacja funkcji, które wyznaczają punkty krzywej Béziera

package
{
    public class DeCasteljauBezier
    {
        import flash.geom.Point;
       
        /**
            Funkcja zwraca wspolrzedne punktu X,
            ktory dzieli odcinek |p0p1| tak, ze |p0X|/|Xp1| = t.
        */

        public static function calculatePoint   (
                                                    p0 : Point,
                                                    p1 : Point ,
                                                    t : Number
                                                ) : Point
        {
            return new Point(p0.x + (p1.x-p0.x) * t , p0.y + (p1.y - p0.y) * t);
        }
       
        /**
            Funkcja zwraca wspolrzedne punktu krzywej Beziera dla parametru t.
        */

        public static function deCasteljauAlgorithm (
                                                        points : Array,
                                                        t : Number
                                                    ) : Point
        {
            if(points.length == 1) return points[0];
            else
            {
                var outputPoints : Array = new Array(points.length - 1);
           
                for( var i : int = 0 ; i < outputPoints.length ; i++)
                {
                    outputPoints[i] = calculatePoint(points[i] , points[i+1] , t);
                }
               
                return deCasteljauAlgorithm(outputPoints , t);
            }
        }
       
        /**
            Funkcja wylicza tablice punktow krzywej Beziera
            o punktach kontrolnych controlPoints z dokladnoscia do parametru deltaT.
        */

        public static function calculateCurvePoints (  
                                                        controlPoints : Array ,
                                                        deltaT : Number = 0.05
                                                    ) : Array
        {
            var outputPoints : Array = new Array();
           
            for ( var t : Number = 0.00 ; t <= 1.00 ; t += deltaT )
            {
                outputPoints.push(deCasteljauAlgorithm(controlPoints , t));
            }
           
            return outputPoints;
        }
    }
}

DeCasteljauBezier.as

oraz Document Class, który jest zmodyfikowaną wersją DC z części drugiej mojego artykułu na temat krzywych Béziera.

package
{
    import flash.display.Sprite;
    import flash.geom.Point;
    import flash.events.MouseEvent;
    import flash.text.TextField;
   
    public class DeCasteljauTestDC extends Sprite
    {
        const       n       : int       = 8;
        const       deltaT  : Number    = 0.02;
       
        private var _points : Array     = new Array(n + 1);
       
       
        public function DeCasteljauTestDC()
        {          
            for( var j : int = 0 ; j < (n + 1) ; j++ )
            {
                var point : Sprite = new Sprite();
                   
                    point.graphics.beginFill(0xad1f47 , 1);
                    point.graphics.drawCircle(0,0,3);
                    point.graphics.endFill();
                   
                    point.addEventListener  (
                                                MouseEvent.MOUSE_DOWN,
                                                mouseDownEventHandler
                                            );
                   
                    point.addEventListener  (
                                                MouseEvent.MOUSE_UP,
                                                mouseUpEventHandler
                                            );
                   
                    point.mouseChildren = false;
                    point.buttonMode    = true;
                   
                    var tf : TextField  = new TextField();
                   
                        tf.autoSize     = 'left';
                        tf.selectable   = false;
                        tf.textColor    = 0xad1f47;
                        tf.text         = 'P_'+ j;
                   
                    point.addChild(tf);
                   
                    point.x = Math.random() * 400;
                    point.y = Math.random() * 400;
                   
                _points[j]  = addChild(point);
            }
        }
       
        /**
            Events handlers
        */

       
        private function mouseDownEventHandler(event : MouseEvent) : void
        {
            (event.target as Sprite).addEventListener   (
                                                            MouseEvent.MOUSE_MOVE,
                                                            mouseMoveEventHandler
                                                        );
            (event.target as Sprite).startDrag(false);
        }
       
        private function mouseUpEventHandler(event : MouseEvent) : void
        {
            (event.target as Sprite).stopDrag();
        }
       
        private function mouseMoveEventHandler(event : MouseEvent) : void
        {
            /**
                Funkcja przy kazdej zmianie pozycji dowolnego z punktow kontrolnych,
                czysci okno i rysuje nowa krzywa.
            */

           
            graphics.clear();
            graphics.lineStyle(1,0x999999 , 1);
            graphics.moveTo(_points[0].x,_points[0].y);

            var controlPoints : Array = new Array();
           
            for( var j : int = 0 ; j < _points.length; j++ )
            {
                controlPoints.push(new Point( _points[j].x , _points[j].y ));
            }

            var points : Array =
            DeCasteljauBezier.calculateCurvePoints(controlPoints,0.01);

            for (var i : int = 0; i < points.length; i++)
            {
                graphics.lineTo (points[i].x,points[i].y);
            }
        }
    }
}

DeCasteljauTestDC.as

Nie zamieszczam obrazka okna, wyświetlającego krzywą, ponieważ wygląda ono identycznie jak w części drugiej – zmienił się tylko sposób implementacji metody rysującej.

Kody źródłowe oraz pliki fla i swf do pobrania stąd.

  • Nov 04

    W części pierwszej pokazałem jak w prosty sposób można narysować krzywą Beziera stopnia trzeciego.

    W tej części pokażę implementację funkcji, które pozwolą rysować krzywe wyższych stopni.

    Wracam do wzoru (w1)

    Widzę, że potrzebuję metody, która zwróci mi wartość wielomianu b.

    Wielomian ten opisuje wzór (w2)

    Aby wyznaczyć jego wartość w zależności od parametrów  i , n , t potrzebuję jeszcze funkcji, która zwróci mi wartość dwumianu Newtona.

    Intuicyjne implementacje tych funkcji zamieszczam poniżej

    package
    {
        import flash.errors.IllegalOperationError;
       
        public class MathUtils
        {
            /**
                Funkcja zwraca wartosc n!
            */

            public static function factor(n : int) : Number
            {
                if(n < 0 )
                {
                    throw new IllegalOperationError
                    ('Wartosc n musi byc dodatnia liczba calkowita!');
                }
                else
                {
                    var result : Number = n;
                   
                    if(n == 0 || n == 1)
                    {
                        result = 1;
                    }
                    else
                    {
                        for(var k : int = n-1 ; k > 0 ; k--) result *= k;
                    }
                   
                    return result;
                }
            }
           
            /**
                Funkcja zwraca wartosc dwumianu Newtona 'n po k'
            */

            public static function binomialCoefficient(n : int, k : int) : Number
            {
                if(n < 0 )
                {
                    throw new IllegalOperationError
                    ('Wartosc n musi byc nieujemna liczba calkowita ( n >= 0 )!');
                }
                else if(k < 0 )
                {
                    throw new IllegalOperationError
                    ('Wartosc k musi byc nieujemna liczba calkowita ( k >= 0 )!');
                }
                else if(k > n)
                {
                    throw new IllegalOperationError
                    ('Wartosc k nie moze byc wieksza od n ( k <= n )');
                }
                else
                {
                    return factor(n) / ( factor(n - k) * factor(k) );
                }
            }
           
            /**
                Funkcja zwraca wartosc wielomianu Bernsteina B_i_n(t).
            */

            public static function bernstainPolynomial(i : int, n : int, t : Number):Number
            {          
                if(i > n || i < 0)
                {
                    return 0;
                }
                else
                {
                    return  binomialCoefficient(n , i)
                            * Math.pow(t , i)
                            * Math.pow((1-t),n-i);
                }
            }
        }
    }

    MathUtils.as

    Stworzyłem także nowy Document Class, w którym krzywa jest rysowana przy zmianie położenia kursora myszki w czasie przeciągania dowolnego punktu kontrolnego.

    package
    {
        import flash.display.Sprite;
        import flash.geom.Point;
        import flash.events.MouseEvent;
        import flash.text.TextField;
       
        public class NBezierTestDC extends Sprite
        {
            const       n       : int       = 8;
            const       deltaT  : Number    = 0.02;
           
            private var _points : Array     = new Array(n + 1);
           
           
            public function NBezierTestDC()
            {
                trace(MathUtils.factor(21));
               
                for( var j : int = 0 ; j < (n + 1) ; j++ )
                {
                    var point : Sprite = new Sprite();
                       
                        point.graphics.beginFill(0xad1f47 , 1);
                        point.graphics.drawCircle(0,0,3);
                        point.graphics.endFill();
                       
                        point.addEventListener  (
                                                    MouseEvent.MOUSE_DOWN,
                                                    mouseDownEventHandler
                                                );
                       
                        point.addEventListener  (
                                                    MouseEvent.MOUSE_UP,
                                                    mouseUpEventHandler
                                                );
                       
                        point.mouseChildren = false;
                        point.buttonMode    = true;
                       
                        var tf : TextField  = new TextField();
                       
                            tf.autoSize     = 'left';
                            tf.selectable   = false;
                            tf.textColor    = 0xad1f47;
                            tf.text         = 'P_'+ j;
                       
                        point.addChild(tf);
                       
                        point.x = Math.random() * 400;
                        point.y = Math.random() * 400;
                       
                    _points[j]  = addChild(point);
                }
            }
           
            /**
                Events handlers
            */

           
            private function mouseDownEventHandler(event : MouseEvent) : void
            {
                (event.target as Sprite).addEventListener(
                                                              MouseEvent.MOUSE_MOVE ,
                                                              mouseMoveEventHandler
                                                          );
               
                (event.target as Sprite).startDrag(false);
            }
           
            private function mouseUpEventHandler(event : MouseEvent) : void
            {
                (event.target as Sprite).stopDrag();
            }
           
            private function mouseMoveEventHandler(event : MouseEvent) : void
            {
                /**
                    Funkcja przy kazdej zmianie pozycji dowolnego z punktow kontrolnych,
                    czysci okno i rysuje nowa krzywa.
                */

               
                graphics.clear();
                graphics.lineStyle(1,0x999999 , 1);
                graphics.moveTo(_points[0].x,_points[0].y);
               
                for ( var t : Number = 0.0 ; t <= 1.0 ; t+= deltaT )
                {
                    var curvePoint : Point = new Point();
                   
                    for( var i : int = 0 ; i < (n + 1) ; i++ )
                    {
                        var P_i : Sprite    = _points[i] as Sprite;
                       
                        var B   : Number    = MathUtils.bernstainPolynomial(i , n , t);
                       
                        curvePoint.x += B * P_i.x;
                        curvePoint.y += B * P_i.y;
                    }
                   
                    graphics.lineTo(curvePoint.x , curvePoint.y);
                }
            }
        }
    }

    NBezierTestDC.as

    Efekt działania programuNBezierTest
    NBezierTest.swf
    Kody źródłowe oraz pliki fla i swf do pobrania stąd.

    Krzywe Béziera – część trzecia

    Nov 04

    Postanowiłem napisać co nieco na temat krzywych Béziera – temat wg mnie ciekawy, choć na pierwszy rzut oka wydający się niełatwym. Przedstawię także przykładową implementację w Action Script 3.0 programu rysującego takie krzywe.

    bezierCurveSample

    Przykładowa krzywa trzeciego stopnia o czterech punktach kontrolnych P1=(50,300) , P2=(100,100) , P3=(300,50) , P4=(350,250), nakreślona we Flash’u.

    1. Wstęp

    Krzywe Béziera, to parametryczne krzywe opracowane niezależnie przez Pierre’a Béziera oraz Paula de Casteljau. Pierwszy z nich rozpowszechnił je poprzez swoje publikacje we Francji w latach 60. i wykorzystywanie ich przy projektowaniu karoserii samochodowych. Drugi natomiast stworzył algorytm rekurencyjny pozwalający obliczać wartości wielomianów Bernsteina, co jest równoważne obliczaniu punktów krzywej Béziera.

    Flash, Illustrator, Photoshop, programy graficzne 3D czy CAD posiadają gotowe narzędzia, które pozwalają tworzyć krzywe B. w sposób o wiele bardziej wydajny niż przedstawiony przeze mnie, jednak myślę, że używanie gotowego narzędzia nie jest tak ciekawe jak napisanie go własnoręcznie.

    2. Matematyczne podstawy

    Dowolna krzywa Béziera n-tego stopnia wyrażona jest wzorem (w1)

    , w którym składowa

    nazywana wielomianami bazowymi Bernsteina ma postać (w2)

    , natomiast

    jest i-tym punktem kontrolnym krzywej.

    Po podstawieniu wyrażenia (w2) do (w1) otrzymujemy wzór ogólny (w3)


    3. Wyprowadzenie wzoru na krzywe Béziera stopnia trzeciego (n = 3)

    Po podstawieniu pod wzór (w3) n=3 i rozwinięciu wyrażenia pod znakiem sigma, otrzymujemy

    Biorąc pod uwagę fakt, że

    oraz

    otrzymujemy ostateczny wzór na krzywą Béziera stopnia trzeciego (w4)

    Aby wyznaczyć współrzędne punktów tej krzywej, wzór (w4) rozbijamy na części, pozwalające obliczyć poszczególne składowe ( x oraz y ).

    Kolejne punkty wyliczamy zmieniając wartość parametru t w zakresie <0 , 1>.

    Przykładowo, chcąc obliczyć składowe punktów leżących na krzywej opisanej wzorem (w4), przedział <0,1> dzielę na n równych części, otrzymując n+1 wartości parametru t.

    Wartości te tworzą zbiór T = { 0/n , 1/n , 2/n, 3/n, … , n/n }. Dla każdego elementu tego zbioru wyliczam wartość wyrażenia B(t), otrzymując w efekcie zbiór n+1 punktów krzywej P = { B(0/n) , B(1/n) , B(2/n) , … , B(n/n) }.

    4. Implementacja funkcji do wyliczania współrzędnych punktów krzywych Béziera stopnia trzeciego na płaszczyźnie

    Aby wyliczyć poszczególne współrzędne punktów leżących na krzywej potrzebuję funkcji, która w zależności od parametru t oraz punktów kontrolnych zwróci współrzędne punktu krzywej.

    package
    {
        import flash.geom.Point;
       
        public class Bezier
        {
           
            /**
            Funkcja zwraca wspolrzedne punktu krzywej Beziera
            trzeciego stopnia, w zaleznosci od paramteru t oraz
            czterech punktow kontrolnych p0,p1,p2,p3.
            */

           
            public static function bezierCurvePoint (
           
                                                        t  : Number,
                                                        p0 : Point,
                                                        p1 : Point,
                                                        p2 : Point,
                                                        p3 : Point
                                                       
                                                    ) : Point
            {
                /**
                Implementacja wzoru (w4)
                */

                const a : Number = 1 * (1-t) * (1-t) * (1-t);
                const b : Number = 3 * t * (1-t) * (1-t);
                const c : Number = 3 * t * t * (1-t);
                const d : Number = 1 * t * t * t;
               
                var px : Number = a * p0.x + b * p1.x + c * p2.x + d * p3.x;
                var py : Number = a * p0.y + b * p1.y + c * p2.y + d * p3.y;
               
                return new Point(px , py);
            }
        }
    }

    Bezier.as

    Aby sprawdzić działanie funkcji bezierCurvePoint tworzę Document Class BezierTestDC.

    package
    {
        import flash.geom.Point;
        import flash.display.Sprite;

        /**
        Testowy Document Class wyswietlajacy  krzywa Beziera.
        */

       
        public class BezierTestDC extends Sprite
        {
            public function BezierTestDC()
            {
                graphics.clear();
                graphics.lineStyle (1,0x000000,1);
               
                const p0:Point  = new Point(0,0);
                const p1:Point  = new Point(100,50);
                const p2:Point  = new Point(200,-50);
                const p3:Point  = new Point(350,350);
               
                const N : int   = 50;
               
                for (var i : int = 0; i&lt;=N; i++)
                {
                    var p : Point = Bezier.bezierCurvePoint(i/N,p0,p1,p2,p3);
                   
                    if(i == 0)
                    {
                        graphics.moveTo (p.x,p.y);
                    }
                   
                    graphics.lineTo (p.x , p.y);
                }
            }
        }
    }

    BezierTestDC.as

    Okno działającego programu. Krzywa narysowana została poprzez połączenie odcinkami 51 punktów wyznaczonych funkcją Bezier.bezierCurvePoint().BezierTest.swfOkno, w którym widać krzywą, narysowaną poprzez połączenie kolejnych punktów wyznaczonych dla t = { 0.00 , 0.02 , 0.04 , … , 1.00 }.

    Kody źródłowe oraz pliki fla i swf do pobrania stąd.

    Krzywe Béziera – część druga