Pieces of Christmas -- part1

Part of the Xmas tasks that I worked on with the other frontenders and the designers this December at Zoocha is an interactive Christmas page for clients.

Zoocha xmas

You can visit the page at https://zoocha.com/christmas2015 (and yes, it is inspired by Species in Pieces by Bryan James )

I mainly do JS so I'll talk about the JS parts...

Tech wise, it is all done in Snap.svg + HTML + CSS. The designers came up with christmas icons built up of 132 triangles, and we are basically animating these triangles from one shape to another using Snap.

When you first go to the page, and at the end when all the animations have finished, you should see a screen with 'random polygons'

random floating polygons

I spent some time figuring out how to get the 'hole' in the middle to work, so for part1, I'll talk about how that works.

The 'hole' is basically an area in the middle of the page where there are no floating polygons so when they come together to form the Xmas icons it looks nicer, and also at the end to highlight the xmas message (which by the way, is just another SVG)

First off, to draw a polygon in Snap:

function drawpolygon(canvas, pointsArr, polygonindex){
    canvas.polygon().attr({
      id: 'poly' + (polygonindex),
      points: pointsArr,
      fill: "#ffffff",
      opacity: 0
    });
  }

Here, the canvas is the svg canvas in which Snap draws. So according to the docs the canvas would be Snap("#svg") (or whatever id you gave to your base svg container).

pointsArr define the polygon-- we are using triangles so it would be [x1, y1, x2, y2, x3, y3]

polygonindex is just used to distinguish individual polygons (we do have 132 of them after all!)

The opacity is set to zero initially so we can fade in the polygons.

Now that we know how to draw the polygons, the next step is to decide on what we want the random snowflakes to do:

  1. appear from the centre of the screen (or in the case of the final screen, from the xmas object) and move to random positions on the screen
  2. leave a 'hole' for the animal/text
  3. drift about their positions randomly, but don't zip across the screen (i.e. small movements only)
  4. there should only be 32 (out of 132) polygons that are snowflakes (otherwise it gets a bit too busy) and they should have random opacities

For a polygon to start from the middle, it is simple-- just assign it a points = [500, 500, 500, 500, 500, 500]. For 32 polygons, we define a pointsArr which is basically an array of points , ie [[500, 500, 500, 500, 500, 500], [500, 500, 500, 500, 500, 500]..., then iterate through the array and draw the 32 of them with drawpolygon. (The 500 because our svg viewBox size is 500 x 500 -- redefine obviously if you have a different svg viewBox size). The initial polygons have zero size so they can 'explode' out.


var pointsArray = initStartingArray();
    pointsArray.map(function(d, i){
      drawpolygon(xMasCanvas, pointsArray[i], i + 1);
    });

(the initStartingArray function basically creates the array of arrays as per above)

Next comes the slightly trickier part -- creating the array of random points for the polygons to move to, making sure that they leave the hole in the centre. I also don't want all the polygons to end up all in one corner of the rectangle.

Basics:

  • to generate random positions, we just need a variation on Math.random()
  • to make sure the polygons don't all end up in one corner, I divided the rectangle into 4 quarters, and distribute the initial points evenly between them.
  • to make the hole, well that's a bit harder to explain, but lets see the full function to make the initial random array first before I attempt to explain that:

function initRandomArry(halfHoleSize){
    //halfHoleSize: how big do you want the 'hole' to be? 
    //e.g for 600 X 600 hole halfHoleSize = 300
    if(!halfHoleSize) halfHoleSize = 250;

    var finalPoints = []; //output array of array of random points
    //x and y coordinates of 1 of the points of the triangle
    var x, y; 
    var i = 0;

    //the following while loops divide the 32 polygons into 4 groups, 
    //each going to 1 quarter of the rectangular svg

    while(i < 8){
      //This generates a random number for both x and y  
      //that is between 0  and 500 i.e. bottom left quarter of the 
      //svg canvas
      x = Math.random() * 500;
      y = 500 + Math.random() * 500;

      // this makes sure the point doesn't go into the 'hole'
      if(x > (500- halfHoleSize) && 500 < y  && y< ( 500 + halfHoleSize) ){
        x = 500 - x;
      }
      //and add the x, y array to the array of random positions
      finalPoints.push([x, y]);
      i++
    }

    //for the bottom right quarter of the canvas
    while (i < 16) {
      x = 500 + Math.random() * 500;
      y = 500 + Math.random() * 500;

      if(x < 500 + halfHoleSize  && y < 500 + halfHoleSize ){
        y = halfHoleSize + 500 + (halfHoleSize - y + 500 );
      }

      finalPoints.push([x, y]);
      i++;
    }

    //for the top right quarter of the canvas
    while (i < 24){
      x = 500 + Math.random() * 500;
      y = Math.random() * 500;

      if(x < 500 + halfHoleSize  && (500- halfHoleSize) < y ){
        x = halfHoleSize + 500 + (halfHoleSize - x + 500 );
      }

      finalPoints.push([x, y]);
      i++;
    }

    //for the top left quarter of the canvas
    while (i < 32) {
      x = Math.random() * 500;
      y = Math.random() * 500;

      if(halfHoleSize < x   && (500- halfHoleSize) < y ){
        y = 500 - y;
      }

      finalPoints.push([x, y]);
      i++;
    }
    //whew, that's 32 points-- can out put the final array now
    return finalPoints
  }

Hopefully you get what's going on for both generating the 32 (more or less ) evenly distributed random points. I'll try to explain what's going on with making sure the point don't fall into the hole with this diagram:

So we want a square hole with length of each side = 2 * hh, inside a canvas of 1000 X 1000. In the top right quarter, the point at (x, y) is inside the hole -- this is true if x < 500 + hh AND y > (500 - hh). To keep it simple, I just 'reflected' the x coordinates about edge of the hole: in the above diagram, a = 500 + hh - x and we are setting the 'reflected' distance a' to be the same as a. So the final coordinates of x will be 500 + hh + a' = 500 + hh + (500 + hh -x )

It's not perfect as the point may be off canvas if hh is quite large... but in the Xmas page the hh is around 250 so I'll live with this for now ;)

Now in the above, we are only generating 1 coordinate per polygon, and we need 3 to make a triangle, and we also need the random polygons to 'float' around-- these effects is achieved by the two following functions:


/*This animates the polygons to the next positions, creating a
 'floating polygons effect' */
function float(randomArray){
    var timeout;
    var newPosArr = getNextPosition(randomArray);
    for (var i = 0; i< 32; i++){
      //fill is always white but opacity will transition 
      // to a random value as the polygons move round
      var fill = "#ffffff";
      var opacityValue = Math.random() * 0.8;

      var x = newPosArr[i][0],
          y = newPosArr[i][1];

      //this generates the other 2 coordinates, basically from 
      // the x, y pair we generate from the getNextPosition
      //  function the 2 other coordinate pairs 
      //just adds a small random value  to them
      var points = [x,y,x+((Math.random()*2) - 1)*50,y+((Math.random()*2) - 1)*50,x+((Math.random()*2) - 1)*50,y+((Math.random()*2) - 1)*50];

      // here you are selecting one particular polygon,
      // as drawn by the drawPolygon function above
      var piece = Snap("#poly" + (i + 1).toString());
      //then you animate the polygon from it's old value
      //to it's new value with Snap's animate function

      piece.animate({ opacity: opacityValue }, 2000, mina.linear);
      piece.animate({ "points": points.toString(), "fill": fill}, 2500, mina.linear);
    }
  },

  // from the random array of points x,y, generate the next 
  //step to move to. positionArr will be 
  //e.g [[x1, y1], [x2, y2]...[x32, y32]]
  function getNextPosition(positionArr, halfHoleSize){
    if(!halfHoleSize) halfHoleSize = 250;
    var outArr = [];
    for(var i = 0; i< positionArr.length; i++){
      var x = positionArr[i][0],
        y = positionArr[i][1];
      //a small movement around the original point, +/- 25px 
      var deltaX = (Math.random() -0.5 ) * 50,
        deltaY = (Math.random() -0.5 ) * 50;
      // the new x and ys
      var outX = (x + deltaX),
        outY = (y + deltaY );

      /*remember to check if the final x or y 
       is larger than the canvas!
       if so find out how much bigger and again reflect it 
       back similar to above 
       e.g. if x == 1200 then the extra is x % 1000
        x -1000 works too of course but x % 1000 to take care
        of all cases. then you take the extra off 1000 to get
        the output x */
      if(outX > 1000){
        outX = 1000 -  outX % 1000
      }

      if(outY > 1000){
        outY = 1000 - outY % 1000
      }
      outArr.push([outX, outY]);
    }
    return outArr
  },

Again, the getNextPosition function has it's imperfections as it should really check for whether the next random point is inside the hole. It doesn't at the moment so there is a tendency for the polygons to diffuse into the hole if you leave it long enough on the final screen!

Hopefully all this makes sense -- and that you have enjoyed wandering down the lanes of a bit of random Maths!