JavaScript

Dinglande objekt

Hur animerar man objekt på en hemsida? Det finns flera val. Ett av de klokare är att använda sig av javascript.

För ett tag sen fick jag möjlighet att jobba tillsammans med Angry Creative på ett spännande projekt – Köttbaren. Sidan designades av Lobby Design och det var vårt ansvar att programmera sidan. Förutom att skapa en webbplats i WordPress samt en ”lightversion” av sidan för mobila enheter gjorde vi något riktigt grymt med den ”vanliga” sidan som visas när man surfar in med datorn. På sidan hänger korv, skinka och annat kött och beställaren tyckte att det skulle vara lite fint om de kunde vaja lite.

Att animera ett objekt i grundrörelser med javascript är inte särskilt svårt, speciellt inte om man är van vid att använda css. Det handlar om att sätta nya värden för hur objekten är placerade. I css kanske du sätter ett värde för hur långt avståndet ska vara mellan vänsterkanten och objektet såhär:

#objektet {
  left: 100px;
}

Ungefär likadant gör man när man sätter ett nytt värde med javascript:

document.getElementById('objektet').style.left = '100px';

I grund och botten är det alltså ganska enkelt att få ett objekt att röra sig. Man kan t ex göra en loop.

for (int i = 0; i < 100; i++) {
  document.getElementById('objektet').style.left = i + 'px';
}

Det finns ändå en del saker att tänka på.

Man kan inte använda sig av ett objekts id innan objektet är skapat. Därför kan det vara en bra idé att lägga in ett onload-event i body-taggen. Då vet du att ditt objekt är skapat när du använder dig av det.

<body onload="StartAnimation();">

Man vill kunna sätta en fördröjning på rörelsen. Om man skulle använda sig av for-loopen ovan skulle objektet snabbt swischa från punkt A till punkt B. I vårt fall vill vi ha en mjuk, långsam rörelse. Jag löste det genom att använda mig av globala variabler som höll reda på startpunkten och vändpunkterna och låta en egen funktion sköta animeringen. (Räkna ut nya värden för objektet och sätta dessa.) Det sista som händer i funktionen är att den anropar sig själv med en viss tidsfördröjning.

function AnimateMeat () {
  // Gör ett steg av animeringen.

  // nTimeOut är en global variabel, gör det lättare att
  // hålla inställningarna på ett ställe.

  t = setTimeout("AnimateMeat()", nTimeOut);
}

Vi vill dessutom kunna låta olika objekt svänga med olika hastighet. Vi löser detta genom att ge varje objekt en egen hastighetsvariabel, t ex såhär.

fSpeed = 0.3;

Nu kan man inte sätta en position till ett decimaltal, men det är lätt fixat med avrundning. Räkna ut ett nytt värde för placeringen och sätt det nya värdet. Eftersom fSpeed är mindre än 1 i det här fallet kommer objektet inte att flyttas alls i alla cykler. Eftersom cyklerna ändå går ganska tätt, så kommer det inte att se hackigt ut ändå. Man får prova sig fram för att hitta de värden som funkar.

// fCurrentX är variabeln vi använder för att hålla reda på var objektet ska placeras i x-led.
fCurrentX += fSpeed; // Öka x-positionen med fSpeed.
document.getElementById('objektet').style.left = Math.floor(fCurrentX) + "px";

Objektet ska byta riktning och rörelsen ska vara avstannande. För att få till en pendlande rörelse måste objektet byta riktning. Det är inte så svårt, det enda man gör är att sätta hastigheten till den negativa hastigheten. Nästa gång man kör animeringsdelen kommer man då att addera ett negativt tal, alltså subtrahera. Och det fina i kråksången är att det funkar lika bra åt andra hållet. När man sätter hastigheten till negativa hastigheten igen kommer den att bli positiv igen. Vi vill dessutom att hastigheten ska avta, att pendeln så småningom ska hamna i ett viloläge. Därför passar vi på att multiplicera hastigheten med en lämplig faktor. Dessutom vill vi flytta slutpunkten något. Inte nog med att rörelsens hastighet ska avta, vi vill också att den inte ska röra sig riktigt lika långt nästa gång.

if (fCurrentX > fEndX) {
  fSpeed = -fSpeed * 0.95;    // Hastigheten sätts till negativ för att vända på rörelsen.
  fEndX -= Math.abs(fSpeed);  // Flyttar vändpunkten något till vänster till nästa gång.
}

Och motsvarande åt andra hållet, förstås. Sen har jag även gjort en koll på om hastigheten är för låg eller om det är för nära mellan start- och slutpunkt. I så fall ska rörelsen upphöra. Det blir så många smårörelser som kan upplevas som hackiga och onaturliga annars.

if ((Math.abs(fEndX - fStartX) < 2) || (Math.abs(fSpeed) < 0.10)) {
  // Sätt nya värden för nästa rörelse.
}

Hur får man objektet att vänta? Vi ville inte att nya animeringar skulle börja så fort den gamla hade slutat, så jag satte upp en wait-variabel. Om wait-variabeln är 0 ska animeringen fortsätta, annars ska man dra ett visst värde från wait-variabeln och hoppar över den aktuella cykeln.

if (nWait <= 0)   // Animera
} else {
  nWait -= nTimeOut;
}

Hur får man objekten att starta slumpvis så att rörelsen inte blir samma varje gång? Genom den förträffliga random-metoden. Random levererar ett slumptal mellan 0 och 1.

nWait = (Math.floor(Math.random()*500) * 10) + 2000;

Först multiplicerar vi med 500 och får alltså ett tal mellan 0 och 500. För att få bort decimaldelen avrundar vi talet och lägger sedan till en nolla. Slumptalet kommer alltså att hamna nånstans mellan 0-5000 ms eller 0-5 seklienter. För att inte riskera att rörelser börjar direkt, så lägger vi till två seklienter. Den nya rörelsen kommer alltså att påbörjas efter 2-7 seklienter.

Om left inte fungerar. Några av objekten kan man inte sätta left på eftersom de ligger som bakgrundsbilder. (Och orsaken till att de ligger som bakgrundsbilder är att de inte ska ge scroll-lister långt ut till höger, men det är en annan historia.) Då kan man sätta background-position. Istället för

document.getElementById('objektet').style.left = Math.floor(fCurrentX) + "px";

skriver man då

document.getElementById('objektet').style.backgroundPosition = Math.floor(fCurrentX) + "px top";

Annars är lösningarna likadana. Tänk på att skriva ut top (eller vilken andra parameter man nu har satt bakgrundsbilden till).

Hur får man till layouten när bredden inte är fast? Man kan t ex utgå från mitten. Mitten brukar vara fast, så även om bredden ändras så kan man förhålla sig till den.

// Tyvärr funkar inte alla sätt att kolla bredden på fönstret likadant i alla browsers.
if (typeof window.innerWidth != 'undefined') {
  nWidth = window.innerWidth;
} else if (typeof document.documentElement != 'undefined'
         && typeof document.documentElement.clientWidth != 'undefined'
         && document.documentElement.clientWidth != 0) {
  // IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document)
  nWidth = document.documentElement.clientWidth;
} else {
  // older versions of IE
  nWidth = document.getElementsByTagName('body')[0].clientWidth;
}

nCenter = parseInt(nWidth / 2);

fCurrentX = nCenter - 395;

Tänk också på att läsa in nya värden om användaren ändrar storlek på sitt webbläsarfönster.

<body onload="StartAnimation();" onresize="Initialize();">

Hur håller man reda på alla objekt när man har flera? Jag använde mig av en multidimensionell array. Tyvärr hängde arrayen inte riktigt med när jag försökte sätta olika värden direkt, så jag använde lokala variabler som jag läste in från arrayen, gjorde alla beräkningar och skrev sedan tillbaka värden till arrayen.

var arrMeat = new Array("salami", "langakorvar", "sawwrap", "kottwrap");

for (var i = 0; i < arrMeat.length; i++) {
  arrMeat[arrMeat[i]] = new Array();
}

// ...

sMeat = "salami";

nMaxDivergence = arrMeat[sMeat]["nMaxDivergence"];
fCurrentX      = arrMeat[sMeat]["fCurrentX"];
fStartX        = arrMeat[sMeat]["fStartX"];
nStartXOrg     = arrMeat[sMeat]["nStartXOrg"];
fEndX          = arrMeat[sMeat]["fEndX"];
nEndXOrg       = arrMeat[sMeat]["nEndXOrg"];
fSpeed         = arrMeat[sMeat]["fSpeed"];
nWait          = arrMeat[sMeat]["nWait"];
bStarting      = arrMeat[sMeat]["bStarting"];

Vad skulle man mer ha kunnat göra? Man skulle ha kunnat jobba med att rotera bilderna, att använda en specifik fästpunkt. Som att hålla en pendel och se var pendeln är fäst. Jag provade även att experimentera med att ändra y-positionen på bilderna, men det blev inte bra, det såg inte naturligt ut.

Internet Explorer ställde som vanligt lite extra krav. Det mest framträdande var att pendlingen såg ut att hacka i IE8 och tidigare. Jag löste det genom att sätta en annan hastighet om browsern var tidigare än IE9. Den här funktionen fanns på stackoverflow.com:

var ie = (function(){

  var undef,
  v = 3,
  div = document.createElement('div'),
  all = div.getElementsByTagName('i');

  while (
    div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
    all[0]
  );

  return v > 4 ? v : undef;
}());

Det låter mig sätta ett eget hastighetsvärde för IE såhär:

if (ie < 9) {
  nTimeOut = 5;
}

Vill du se hela skriptet? Varsågod!

Mikael är grundare av emmio.se. Han har jobbat med webbutveckling sedan 1999 och tycker det är extra kul att få roliga tekniker att göra nyttiga saker.