2017年8月16日星期三

Create Your Own Maze In Your Browser With D3.js (2) Generating Animation

In "Create Your Own Maze In Your Browser With D3.js (1)", we have known the secret of how to generate a random maze with JavaScript and D3.js. Today, I am going to push it a little bit further: to show the how process of maze generating.


As I mentioned before, you can use Recursive Process or not to achieve the random maze generator. For an animation show, we'd better keep recursive process away. Yes, based on my last post, the maze generator is pretty nature to support animation. Here is the plan:


  • Step 1: show all the room soundly with four walls in the maze;
  • Step 2: show a red spot at the beginning room to generate the maze;
  • Step 3: change the room colour as we visit it as find a neighbour room as current room and move red spot to the current room.
  • Step 4: use a interval timer to execute Step 3 every 100 milliseconds until all the rooms have been visited.

As we trace the visited rooms with our own stack, it is easy to drive the animation step by step and each step will process one room only. As you can see in the SVG animation in the post, red spot will tell you where is the current room and visited room will be painted with a different colour.

Have to say, there is no much different between this animation version and last static version. D3.js can really help us focus on the data and logic and make the SVG drawing and animation a piece of cake.

Here are the differences you may want to know:


  • We need to render the maze at the very beginning and need to update the maze after every step. So in this animation version, we have renderMaze() and updateMaze() to finish these two different jobs. Be careful, D3.js treats generating new html elements and updating html element with data in different ways.


var renderMaze = function(){

    // this is data binding for first layer of array.
    var row = svg.selectAll(".row")
        .data(matrix)
        .enter().append("g")
        .attr("class", "row");

    // this is data binding for second layer of array. Every room is a group for multi-shapes.
    var column = row.selectAll(".square")
        .data(function(d) { return d; })
        .enter().append("g").attr("class","square");

    // append rect as room background.
    column.append("rect")
        .attr("class", function(d) { return d.xPos + "-" + d.yPos; })
        .attr("x", function(d) { return d.xPos; })
        .attr("y", function(d) { return d.yPos; })
        .attr("width", roomWidth)
        .attr("height", roomHeight)
        .style("fill", roomBackground);

    // append left wall;
    column.append("line")
        .attr("class", "left")
        .attr("x1", function(d) { return d.xPos; })
        .attr("y1", function(d) { return d.yPos; })
        .attr("x2", function(d) { return d.xPos;})
        .attr("y2", function(d) { return d.yPos + roomHeight;})
        .attr("stroke-width", function(d) { return d.left; })
        .style("stroke", wallColour);

    // append right wall;
    column.append("line")
        .attr("class", "right")
        .attr("x1", function(d) { return d.xPos + roomWidth; })
        .attr("y1", function(d) { return d.yPos; })
        .attr("x2", function(d) { return d.xPos + roomWidth;})
        .attr("y2", function(d) { return d.yPos + roomHeight;})
        .attr("stroke-width", function(d) { return d.right; })
        .style("stroke", wallColour);

    // append top wall;
    column.append("line")
        .attr("class", "top")
        .attr("x1", function(d) { return d.xPos; })
        .attr("y1", function(d) { return d.yPos; })
        .attr("x2", function(d) { return d.xPos + roomWidth;})
        .attr("y2", function(d) { return d.yPos;})
        .attr("stroke-width", function(d) { return d.top; })
        .style("stroke", wallColour);

    // append bottom wall;
    column.append("line")
        .attr("class", "bottom")
        .attr("x1", function(d) { return d.xPos; })
        .attr("y1", function(d) { return d.yPos + roomHeight; })
        .attr("x2", function(d) { return d.xPos + roomWidth;})
        .attr("y2", function(d) { return d.yPos + roomHeight;})
        .attr("stroke-width", function(d) { return d.bottom; })
        .style("stroke", wallColour);

    // we need to current flag to show where is the current room.
    svg.append("circle")
        .attr("class", "current")
        .attr("cx", spot.xPos)
        .attr("cy", spot.yPos)
        .attr("r", spot.r)
        .style("fill", spot.color);

}

var updateMaze = function(){

    // this is data binding for first layer of array.
    var row = svg.selectAll(".row")
        .data(matrix)
        .attr("class", "row");

    // this is data binding for second layer of array. Every room is a group for multi-shapes.
    var column = row.selectAll(".square")
        .data(function(d) { return d; });

    // append rect as room background.
    column.selectAll("rect")
        .attr("x", function(d) { return d.xPos; })
        .attr("y", function(d) { return d.yPos; })
        .attr("width", roomWidth)
        .attr("height", roomHeight)
        .style("fill", function(d) { return d.backgroupd;});

    // append left wall;
    column.selectAll(".left")
        .attr("x1", function(d) { return d.xPos; })
        .attr("y1", function(d) { return d.yPos; })
        .attr("x2", function(d) { return d.xPos;})
        .attr("y2", function(d) { return d.yPos + roomHeight;})
        .attr("stroke-width", function(d) { return d.left; })
        .style("stroke", wallColour);

    // append right wall;
    column.selectAll(".right")
        .attr("x1", function(d) { return d.xPos + roomWidth; })
        .attr("y1", function(d) { return d.yPos; })
        .attr("x2", function(d) { return d.xPos + roomWidth;})
        .attr("y2", function(d) { return d.yPos + roomHeight;})
        .attr("stroke-width", function(d) { return d.right; })
        .style("stroke", wallColour);

    // append top wall;
    column.selectAll(".top")
        .attr("x1", function(d) { return d.xPos; })
        .attr("y1", function(d) { return d.yPos; })
        .attr("x2", function(d) { return d.xPos + roomWidth;})
        .attr("y2", function(d) { return d.yPos;})
        .attr("stroke-width", function(d) { return d.top; })
        .style("stroke", wallColour);

    // append bottom wall;
    column.selectAll(".bottom")
        .attr("x1", function(d) { return d.xPos; })
        .attr("y1", function(d) { return d.yPos + roomHeight; })
        .attr("x2", function(d) { return d.xPos + roomWidth;})
        .attr("y2", function(d) { return d.yPos + roomHeight;})
        .attr("stroke-width", function(d) { return d.bottom; })
        .style("stroke", wallColour);

    // we need to current flag to show where is the current room.
    svg.select(".current")
        .attr("cx", spot.xPos)
        .attr("cy", spot.yPos)
        .attr("r", spot.r)
        .style("fill", spot.color);

}


  • You need to start a interval timer to drive the process forward, here is the sample:
var performGenStep = function(){
    visitRoom();
    updateMaze();
}

// By visiting the room, we can go to next room until all the rooms have been visited.
var genRandomMaze = function(){
    var firstX = Math.floor(Math.random() * row);
    var firstY = Math.floor(Math.random() * col);
    console.log("First room to start Row - Col: " + firstY + " - " + firstX);

    var room = matrix[firstY][firstX];
    stack.push(room);

    timer = $interval(performGenStep, 100);
    
}

The last thing you might need to pay attention is the new svg element in the maze, red spot. I encourage to press F12 to check the JavaScript source code in this blog page, and find out places we need to handle the red spot during the maze generating. Let me know if you need some help.

Warning: as the maze is 20x20, it might take a long time to finished the whole generating process.





没有评论:

发表评论