Kategoriarkiv: Programmering

Diverse programmeringsrelaterede artikler

Microsoft Silver Partner ISV

Hinnerup Net A/S er i maj 2011 blevet udnævnt som Microsoft Silver Partner ISV (Independent Software Vendor) med speciale i softwareløsninger.

Vi har investeret såvel tid og energi i at skærpe og målrette vore kompetencer indenfor Microsoft’s teknologier og platforme, og vi belønnes nu for den ekspertise og viden vi har opnået.

Dette betyder du som kunde kan regne med at du får:

  • leveret kvalitetsløsninger indenfor Microsoft’s teknologi og platform
  • softwareudviklere med optimal softwarekendskab og kompetence
  • et samarbejde med en softwareleverandør der er en anerkendt Microsoft Partner

ADO.NET vs. LinQ to SQL

Fordele

Der er flere fordele ved LinQ to SQL over ADO.NET. Nogle eksempler er følgende:

  • Ingen brug for at skrive triviel kode til databasekald.
  • Stærke typer gør at flere fejl fanges ved compile-time.
  • Visual Studio intellisense resulterer i kortere udviklingstid.
  • Lambda expressions resulterer i kortere udviklingstid og mere kompakt og forståelig kode.

Hastighed

I de fleste situationer er der ikke grund til at spekulere på om LinQ to SQL har værre performance end ADO.NET. De største performanceforbedringer findes typisk tre andre steder:

  1. Udførelse af operationer på data på SQL serveren når det er muligt.
  2. Benyttelse af fornuftige indeks og evt. midlertidige tabeller
  3. Reducering af mængden af data som skal overføres til applikationen.

Opsætning af LinQ to SQL database context

Opret et projekt af typen Console.

Højreklik på projektet, vælg Add New Item -> Data -> LINQ to SQL Classes. Sæt Name til “MyDatabase.dbml” og klik Add.

Klik på Server Explorer.

Højreklik på Data Connections og vælg Add Connection…

Skriv “.\SQLEXPRESS” i Server name og klik på Refresh. Der bør ikke meldes om fejl.

Skriv “MyDatabase” i New database name og klik OK.

Opret en tabel Customers med følgende struktur:

create table Customers (ID int NOT NULL IDENTITY, FirstName nvarchar(100), LastName nvarchar(100), BirthDate datetime, Active bit, constraint Customers_ID primary key (ID))

Træk tabellen Customers over i MyDatabase.dbml vinduet og gem filen.

Indsættelse af entiteter med ADO.NET

Vi fodrer et SqlCommand objekt med et SQL statement og udfører kommandoen på et åbent SqlConnection objekt.

private static void AddCustomer(SqlConnection connection, string firstName, string lastName, DateTime birthDate, bool active)
{
    var command = new SqlCommand("insert into Customers (FirstName, LastName, BirthDate, Active) VALUES (@FirstName, @LastName, @BirthDate, @Active)", connection);
    command.Parameters.Add(new SqlParameter("@FirstName", firstName));
    command.Parameters.Add(new SqlParameter("@LastName", lastName));
    command.Parameters.Add(new SqlParameter("@BirthDate", birthDate));
    command.Parameters.Add(new SqlParameter("@Active", active));
    command.ExecuteNonQuery();
}

Som kan ses, så kræver det en del triviel kode.

Udtræk af entiteter med ADO.NET

Vi fodrer en connection string til et SqlConnection objekt, åbner forbindelsen og indsætter nogle entiteter. For at hente entiteterne frem igen, fodrer vi SQL til SqlCommand, opretter en SqlDataReader, gennemgår hver række samt henter værdien af hvert felt vha. feltnavnindeks. Læg mærke til det trivielle kode vi benytter til bare at konvertere til den rette datatype. Koder vi fejl her, så opdager vi det sandsynligvis først ved run-time ved test (hvis vi da sørger for at teste kodevejen med fejlen) eller (endnu værre) ved run-time i produktionmiljøet. Typefejl opstår ikke så ofte med LinQ to SQL, men kan ske hvis .dbml filen ikke er opdateret til databasestrukturen.

static void Ado()
{
    using (var connection = new SqlConnection(ConnectionString))
    {
        connection.Open();
        AddCustomer(connection, "Hans", "Christian", new DateTime(1960, 10, 4), true);
        AddCustomer(connection, "George", "Kohl", new DateTime(1974, 5, 7), true);
        AddCustomer(connection, "Marie", "Muse", new DateTime(1986, 2, 8), false);
        var command = new SqlCommand("select FirstName, LastName, BirthDate, Active from Customers", connection);
        var customersReader = command.ExecuteReader(CommandBehavior.CloseConnection);
        while (customersReader != null && customersReader.Read())
            Console.WriteLine(String.Format("{0} {1} {2} ({3})",
                (string)customersReader["FirstName"],
                (string)customersReader["LastName"],
                ((DateTime)customersReader["BirthDate"]).ToString("dd-MM-yyyy"),
                (bool)customersReader["Active"]));
    }
}

Indsættelse og udtrækning af objekter med LinQ to SQL

Med LinQ to SQL opretter vi et MyDatabaseDataContext objekt som svarer til SqlConnection objektet i ADO.NET. Vi fodrer objektet med en connection string.

Som det kan ses så er koden til indsættelse så triviel at vi ikke behøver at pakke det ind i egen metode for at få overskuelig kode. Det kræver kun to linjer kode. Vi opretter et Customer-objekt, fodrer objektet til <datakontekst>.<objekt-type>s.InsertOnSubmit() samt kalder <datakontekst>.SubmitChanges() for at igangsætte ændringerne i databasen.

Med LinQ to SQL kan vi udtrække data på to måder.

1) SQL lignende syntax. Struktur:

Denne form kan være god at bruge i de lidt mere komplekse tilfælde med joins. LinQ to SQL sørger for at hentes mindst muligt data fra databasen.

var variable1 = from <id1> in <datakontekst>.<tabelnavn1>s
                [join <id2> in <datakontekst>.<tabelnavn1>s on <id1>.<key1> equals <id2>.<key2>]
                [where]
                [<id1>.<feltnavn1>.<metode>(<parametre>)]
                [&& <id1>.<feltnavn2> <operator> <værdi>]
                [orderby <id1>.<feltnavn1> [ascending|descending]]
                select <id1>;

Hvis meget kompleks datamanipulation eller udtræk er nødvendigt, så kan man med fordel oprette en stored procedure og kalde denne ved brug af LinQ to SQL. Metoder til at kalde stored procedures placeres på datakontekstobjektet.

Det er også muligt at instansiere et objekt af en type som opbygges on-the-fly ved at ændre “select <id1>” til:

select new
{
    <feltnavn1> = <udtryk>,
    <feltnavn2> = <udtryk>
};

2) Metodekald med lambda expressions. Struktur:

var customers2 = <datakontekst>.<tabelnavn>s.Where(<id> => [<id>.<feltnavn>.<metode>(<parametre>)] <operator> 
[<id>.<feltnavn> <operator> <value>]);

Vi kan nu bare iterere igennem resultatsættet som består af stærkt typede objekter f.eks. med foreach. Eksempel:

static void Linq()
{
    using (var db = new MyDatabaseDataContext(ConnectionString))
    {
        db.Customers.InsertOnSubmit(new Customer { FirstName = "LinQ", LastName = "Rocks", BirthDate = new DateTime(2006, 4, 11), Active = false});
        db.SubmitChanges();

        var customers1 = from c in db.Customers
                        where c.FirstName.StartsWith("H") &&
                              c.Active == true
        orderby c.BirthDate ascending
                        select c;

        foreach (var customer in customers1)
            Console.WriteLine(String.Format("1: {0} {1} {2} ({3})",
                customer.FirstName,
                customer.LastName,
                customer.BirthDate.ToString(),
                customer.Active));

        var customers2 = db.Customers.Where(c => c.FirstName.StartsWith("H") && c.Active == true);
        foreach (var customer in customers2)
            Console.WriteLine(String.Format("2: {0} {1} {2} ({3})",
                customer.FirstName,
                customer.LastName,
                customer.BirthDate.ToString(),
                customer.Active));

        var customers3 = from c in db.Customers
                        where c.Active == false
                        select new
                        {
                            FullName = c.FirstName + " " + c.LastName,
                            BirthDate = c.BirthDate,
                        };

        foreach (var customer in customers3)
            Console.WriteLine(String.Format("3: {0} {1}",
                customer.FullName,
                customer.BirthDate.ToString()));
    }
}

Resultatet fra Linq() metoden er:

1: Hans Christian 04-10-1960 00:00:00 (True)
2: Hans Christian 04-10-1960 00:00:00 (True)
3: Marie Muse 08-02-1986 00:00:00
3: LinQ Rocks 11-04-2006 00:00:00

Animeret Javascript menu

Introduktion

For noget tid siden fik jeg mulighed for at lege lidt mere end normalt med Javascript. Resultatet blev en animeret Javascript menu, som jeg vil beskrive i dette indlæg.

Udgangspunktet for menuen kan ses i skitsen herunder:

Hvert menu-punkt i menuen består af et billede med et anker omkring. Det midterste billede er det aktive billede – hvis der klikkes på det, navigeres der til linket defineret af ankeret. Klikkes der på et af de andre billeder, bliver det aktivt, og menuen roteres, så det aktive billede altid er centreret.

Et synligt menu-punkt er til enhver tid tilknyttet en position i menuen.

Menuen udvikles som en JQuery widget, idet det herved bliver nemt at anvende menuen. Den basale widget struktur kan ses herunder:

(function($) {
    options: {
        nVisibleItems: 5,
        visibilityFactor: 0.5,
        nFrames: 60,
        speed: 200,
	smoothTransition: true
    },

    _create: function() {
        ...
    },
    ...    
})(jQuery);

I ovenstående stump kode erklæres først en anonym funktion, der tager én parameter $.Funktionen kaldes herefter med jQuery som parameter værdi. Herved undgåes evt. sammenfald med andre Javascript biblioteker, der også benytter $, som f.eks. Prototype.

Første skridt i den anonyme funktion er at tilføje vores widget under navnet ui.rotator til jQuery objektet. Funktionen _create fungerer som konstruktør for en jQuery widget. Den pre-fixede ‘_’ er jQuery’s notation for en privat funktion.

Options objektet inderholder parametre til konstruktøren, som kan sættes når widget’en instantieres. Det er her muligt at angive antallet af synlige billeder i menuen, hvor stor en del af hvert billede der er synligt, hvor mange frames der går på en enkelt rotation, hastigheden for en rotation, samt hvorvidt der ved overgang fra inaktivt til aktivt menupunkt vises en “blød” animation. Sidstnævnte er beskrevet i detaljer sidst i artiklen.

Initialisering af menu

For at implementere menuen kan vi nøjes med at kigge på de synlige billeder samt de to billeder lige uden for den synlige del af menuen (markeret med stiplede linjer på figuren ovenfor). Et billedes position i menuen er en quadruple (top, left, width, zIndex), og i det følgende beskrives hvordan disse beregnes.

Beregning af menuens bredde

Center-positionen antages at have bredden w, og bredden på de resterende positioner gøres afhængige heraf. Dette klares ved at benytte en skaleringsfaktor a og sætte bredden af positionen i pladser fra midten til værdien .

Hvis vi antager, at v er synlighedsfaktoren, som angiver hvor stor en del af hver position, der skal være synlig i forhold til nærmeste position i den aktive positions retning, kan vi beregne den faktiske bredde af menu’en ud fra formlen:

Hvor n er antallet af positioner til venstre for midten. Summen til sidst i udtrykket er en geometrisk række og har derfor en kendt værdi, der kan udtrykkes ved:

Benyttes dette i ligningen fra før får vi:

Givet a kan vi altså beregne Witems. Målet må altså være at finde en skaleringsfaktor a, således at afstanden er mindre end et givet .

Vi kan implementere beregningen af bredden simpelt som i nedenstående funktion:

  _getEstimatedMenuWidth: function(scalingFactor) {
    var w = this.centerImageWidth,
    v = this.options.visibilityFactor,
    n = this.centerPositionIndex;

    var width = w * (1 + 2 * v * ((1 - Math.pow(scalingFactor, n)) / (1 - scalingFactor) - 1));
    return width;
},

Estimering af skaleringsfaktor

For at finde en tilnærmet værdi for skaleringsfaktoren kan vi udnytte følgende:

Vi leder efter en skaleringsfaktor mellem 0 og 1. Starter vi med ½, kan vi beregne den tilnærmede bredde Witems. Hvis afstanden mellem Wmenu og Witems er tilpas lille, er vi tilfredse. Baseret på ovenstående formel leder vi videre i intervallet (0;½) eller (½;1), alt efter om den er større eller mindre. Vi vælger blot en ny kandidat som skaleringsfaktor, defineret som midterpunktet i det nye interval. Herved opnåes en logaritmisk søgning, og vi kan finde en tilnærmet løsning i få skridt.

Søgningen implementeres rekursivt som nedenstående, hvor der benyttes en threshold på 1 pixel:

  _getEstimatedScalingFactor: function(min, max) {			
    var scalar = min + (max - min) / 2,					
    estimatedWidth = this._getEstimatedMenuWidth(scalar);

    if (this._isThresholdSatisfied(estimatedWidth)) {
      return scalar;
    }

    if (estimatedWidth < this.viewWidth) {
      return this._getEstimatedScalingFactor(scalar, max);
    } 

    return this._getEstimatedScalingFactor(min, scalar);
  },

Venstre-placering af positioner

For at kunne placere positionerne korrekt i forhold til hinanden, er det nødvendigt for os at kende bredden på hver position. Da vi kun kan angive bredden på billederne i hele pixels, og da floating-point repræsentation er unøjagtig, må vi dog påregne et vist præcisionstab, når vi benytter den estimerede skaleringsfaktor til at beregne positionernes bredder. Vi skal altså håndtere, at summen af bredderne for den synlige del af hver position, ikke nødvendigvis ender på Wmenu.

Dette kan vi gøre ved at starte med at placere den yderste position til venstre (som ikke er synlig). Dernæst placeres de resterende positioner med offset heri, dog med undtagelse af den aktive. Den aktive position placeres i midten af menuen, hvorved evt. overskydende eller manglende pixels vil blive ligeligt fordelt på begge sider af den aktive position.

Venstre-offset’et for positionerne kan beregnes som følger, hvor n er antallet af positioner til venstre for den aktive position, og p er det totale antal positioner:

Top-placering af positioner

Top-placeringen for hver position kan vi finde ved først at beregne forholdet mellem positions bredde og den aktive positions bredde. Vi kan så benytte dette forhold til at finde positionens nye højde ud fra højden på den aktive position. Halverer vi højden, har vi top-placeringen. Beregningen ses herunder:

Beregning af z-index

Vi mangler nu kun at beregne zIndex for hver position for at kunne beskrive positionerne fuldstændigt. Dette beregnes dog let ved at lade zIndex for den aktive position være n, mens zIndex for de resterende positioner aftager med 1 for hver plads fra den aktive position.

I koden er hele processen implementeret som følger:

_Position: function(top, left, width, zIndex) {
  return { "top": top, "left": left, "width": width, "zIndex": zIndex };
},
		
_getPositions: function() {
  var p = [],
  v = this.options.visibilityFactor,
  n = this.nPositions,
  m = this.centerPositionIndex,
  W = this.viewWidth,
  w = this.centerImageWidth,
  h = this.centerImageHeight,
  imageWidths = this._getImageWidths();

  p[m] = new this._Position(0, Math.floor((W - w) / 2), w, m);
  p[0] = new this._Position(Math.floor((h - imageWidths[m - 1]*h / w) / 2), 0 - Math.floor(imageWidths[m - 1]*v), imageWidths[m - 1], 0);
  
  for (var i = 1; i < n; i++) {
    if (i == m) {					
      continue;
    }

    if (i < m) {
      p[i] = new this._Position(Math.floor((h - imageWidths[m - i - 1]*h / w) / 2), p[i - 1].left + Math.floor(p[i - 1].width * v), imageWidths[m - i - 1], p[i - 1].zIndex + 1);				
    } else {
      p[i] = new this._Position(p[n - i - 1].top, W - p[n - i - 1].left - p[n - i - 1].width, p[n - i - 1].width, p[n - i - 1].zIndex);
    }
  }

  return p;
},

Animering

Nu hvor positionernes hvile-placering er på plads i menuen, mangler vi bare at beregne positionernes placering under hver rotation, herefter kaldet frames. Antallet af frames mellem hver positions hvile-placering er angivet i options objektet. Vi beregner kun frames i venstre side af menuen, og spejler så disse til højre side. Beregningen er i princippet den samme for både top, left og width. For at komme fra position i til i+1 skal vi blot dele differencen mellem top, left, og width værdierne med antallet af frames. Disse fordeles herefter ligeligt ud på frames mellem position i og i+1. I koden ser det ud som følger:

_getAnimationSteps: function() {
  var positions = this._getPositions(),
                       animationSteps = [];

  for (var i = 0; i < this.nPositions - 1; i++) {
    animationSteps[i*this.options.nFrames] = positions[i];
    for (var j = 1; j < this.options.nFrames; j++) {
      animationSteps[i*this.options.nFrames + j] = new this._Position(
        this._getAnimationStep("top", positions, i, animationSteps, j),
	this._getAnimationStep("left", positions, i, animationSteps, j),
	this._getAnimationStep("width", positions, i, animationSteps, j),
	(j / this.options.nFrames < 0.5 ? positions[i].zIndex : positions[i + 1].zIndex)
      );
    }
  }
  
  animationSteps[(this.nPositions - 1)*this.options.nFrames] = positions[this.nPositions - 1];			
  return animationSteps;
},

_getAnimationStep: function(propertyName, positions, i, animationSteps, j) {
  return positions[i][propertyName] + Math.floor(j*(positions[i + 1][propertyName] - positions[i][propertyName]) / (this.options.nFrames));
},

Afbilding af positioner til billeder

For at kunne placere billederne korrekt i menuen er det nødvendigt at kunne projektere de beregnede positioner ned på en delmængde af billederne. For at kunne gøre dette benyttes et offset, der angiver hvor i listen af billeder, den første position skal placeres. Alle billeder, der ikke matches med en position, sættes til display: none. Koden hertil ses nedenfor:

_drawImages: function() {
  var positionOffset = this.animationState.getCurrentPositionOffset();
  for (var i = 0; i < this.images.length; i++) {
    var positionIndex = i - positionOffset;
    if (positionIndex >= 0 && positionIndex < this.nPositions - 1) {
      var position = this.animationSteps[positionIndex*this.options.nFrames + this.animationState.getCurrentFrame()];
      $(this.images[i]).css( "width", position.width );
      $(this.anchors[i]).css( {top: position.top, left: position.left, zIndex: position.zIndex, display: "block" } );
    } else {
      $(this.anchors[i]).css({ display: "none" });
    }
  }
  this._moving();
},

Det nuværende positionoffset beregnes som det aktive billedes indeks fratrukket indekset for den midterste position. Dette lægger op til, at vi blot skal huske på indekset for det aktive billede. Men det giver nogle lidt knudrede beregninger, idet skiftet fra f.eks. position i frame 29 til position i+1 frame 0 betyder, at det aktive billedes indeks skal tælles 1 ned. Dvs. når frameindekset stiger, så falder det aktive billedes indeks. Endvidere skal man angive både det aktive billede og frameindekset for at rotere menuen til en bestemt position. Det er bedre med et enkelt indeks, hvor det aktive billede og frame-indekset kan beregnes fra.

Vi kan definere animationsindekset s som , hvor n er antallet af billeder, og m er antallet af frames pr. rotation. Herudover kan vi definere følgende funktioner:

  • Billedeindeks til animationsindeks:
  • Animationsindeks til billedeindeks:
  • Animationsindeks til frameindeks:

Af ovenstående funktioner fremgår det, at for hver gang animationsindekset forøges med m, trækkes der 1 fra det aktive billedes indeks, hvilket er den ønskede opførsel.

Koden til at holde styr på indekset kan ses herunder:

_AnimationState: function(nImages, nFrames, speed, currentImage, centerPositionIndex) {
  var imageToAnimationStep = function(image) {
         return (nImages - image) * nFrames;
       },
       currentAnimationStep = imageToAnimationStep(currentImage),
       lastUpdated = null;
			
  this.getCurrentImage = function() {
    return nImages - Math.floor(currentAnimationStep / nFrames);
  }
			
  this.getCurrentPositionOffset = function() {
    return this.getCurrentImage() - centerPositionIndex;
  }
			
  this.getCurrentFrame = function() {
    return currentAnimationStep % nFrames;
  }		
			
  this.getAnimationStep = function() {
    return currentAnimationStep;
  }
			
  this.update = function(targetImage) {	
    var targetAnimationStep = imageToAnimationStep(targetImage),
         direction = 0;
    if (targetAnimationStep === currentAnimationStep) {
      lastUpdated = null;
      return;
    } 
				
    direction = (currentAnimationStep > targetAnimationStep ? -1 : 1);
				
    if (lastUpdated === null) {
      lastUpdated = (new Date()).getTime();
      currentAnimationStep += direction;
      return;
    }
				
    var now = (new Date()).getTime();
    var diff = now - lastUpdated;
    lastUpdated = now;
			
    var offset = Math.floor(diff / this.getFrameSpeed());
				
    currentAnimationStep += direction * offset;
				
    if ((direction < 0 && currentAnimationStep > targetAnimationStep) ||
        (direction > 0 && currentAnimationStep < targetAnimationStep)) {
      return;
    }
				
    currentAnimationStep = targetAnimationStep;
    lastUpdated = null;
  }

  this.setAnimationStep = function(animationStep) {
    currentAnimationStep = animationStep;
  }
			
  this.getMaxAnimationStep = function() {
    return imageToAnimationStep(0);
  }
			
  this.getMinAnimationStep = function() {
    return imageToAnimationStep(nImages - 1);
  }
			
  this.isSettled = function() {
    return this.getCurrentFrame() === 0;
  },
			
  this.getFrameSpeed = function() {
    return speed / nFrames;
  }
},

Update funktionen i AnimationState objektet tester på, om det er nødvendigt at springe frames over under animeringen. Browsere er ikke altid lige præcise i deres timer funktioner, og de udfører placeringen af billederne i hvert skridt med variabel hastighed. For at undgå en hakkende og sløv rotation af menuen sørger Update funktionen for altid at vise den frame, der burde være den aktive frame jvf. tiden siden sidste opdatering.

Med alt det ovenstående på plads, kan vi nu forholdsvist nemt starte en animation, der aktiverer et vilkårligt billede i menuen. Funktionen udføres af nedenstående kode.

centerImage: function(targetImage) {
  targetImage = parseInt(targetImage);
			
  if (!this.animationState.isSettled()) {
    return;
  }
		
  var self = this;
  this.animationInterval = setInterval(
                                    function() {
				      self.animationState.update(targetImage);
				      self._drawImages();
				      if ((targetImage === self.animationState.getCurrentImage()) && self.animationState.isSettled()) {
				        clearInterval(self.animationInterval);
					self.animationInterval = null;
					self._itemCentered();
				      }
				    }, Math.ceil(this.animationState.getFrameSpeed())
  );
},

Glidende overgang mellem aktiv og inaktiv position

Den sidste ting, der mangler, er nu at lave en pæn overgang mellem de aktive billede og dets to naboer. Indtil videre vil billederne glide igennem hinanden, når det aktive billede bliver mindre, og det kommende aktive billede bliver større. Ved at ændre på animationen for den aktives billedes rotation, sådan at de to billeder flugter hinanden, når de er lige store, får vi et langt pænere skift. Dette gøres ved at tilføje en ny option smoothTransition til options objektet, samt nedenstående kode:

_smoothTransition: function() {
  var leftPosition = (this.centerPositionIndex - 1) * this.options.nFrames,
       centerPosition = this.centerPositionIndex * this.options.nFrames,
       centerFrame = Math.floor(this.options.nFrames / 2),
       p = this.animationSteps,
       overlap = p[centerPosition + centerFrame].width - (p[centerPosition + centerFrame].left - p[leftPosition + centerFrame].left),
       var centerFrameLeftTarget = p[leftPosition + centerFrame].left - overlap / 2,
       var firstOffset = (centerFrameLeftTarget - p[leftPosition].left) / centerFrame,
       var secondOffset = (p[leftPosition + this.options.nFrames - 1].left - centerFrameLeftTarget) / (this.options.nFrames - centerFrame - 1);
			
  for (var i = 1; i < this.options.nFrames; i++) {
    if (i <= centerFrame) {
      p[leftPosition + i].left = Math.floor(p[leftPosition].left + i*firstOffset);
    } else {
      p[leftPosition + i].left = Math.floor(p[leftPosition + centerFrame].left + (i - centerFrame)*secondOffset);
    }
    p[centerPosition + this.options.nFrames - i].left = this.viewWidth - p[leftPosition + i].left - p[leftPosition + i].width;
  }
},

Den færdige menu

Vores widget eksponerer to public events moving og itemCentered, der aktiveres efter _drawImage, og centerImage funktionerne er blevet kørt. Udover centerImage funktionen, som er public, tilbydes også setAnimationStep, med hvilken man direkte kan sætte animationsindekset, samt value som returnerer det nuværende animationsindeks. Disse events og funktioner kan benyttes til f.eks. at tilknytte en slider til menuen.

Et eksempel på brug kan downloades her. Scriptet kan downloades her.

Den endelige menu kan ses herunder (klik på menu-billederne der ikke er i midten for at rotere frem til det, eller klik på det centrerede billede for at navigere til siden):