-
Notifications
You must be signed in to change notification settings - Fork 0
Integrating the new Design
All of the code that draws stuff on the screen will be written in JavaScript. This may seem strange but it will clearly illustrate the differences between rendering and state management. Instead of the DOM and HTML, we will use JavaScript as our rendering technology.
It is typical to work on a project with multiple people. Doing all of the drawing in JavaScript will also show that it is possible to divide the work of building an application between people with different skills and technology preferences. By clearly separating rendering from application logic, Pedestal makes this kind of division of labor easy to accomplish.
This section of the tutorial will go over all of the JavaScript which handles the drawing for the game. As we go through all of this code, keep in mind that it could all be written ClojureScript.
All of the drawing that the rendering code is supported by the excellent Raphael JavaScript library.
Before we start writing the code, let's first set up our project so that we have a place to work on the UI code in isolation. We start by adding a new page to the Design area of the project.
In the file tools/public/design.html add the following link
<li><a href="/design/game.html">Game</a></li>and then add the file app/templates/game.html.
<_within file="application.html">
<div id="content">
<div class="row-fluid" template="tutorial" field="id:id">
<div id="game-board"></div>
<!-- We need this because of a bug in Pedestal. Remove when
fixed. -->
<div field="content:something"></div>
</div>
</div>
<script id="script-driver" src="/game-driver.js"></script>
</_within>Download raphael.js and place it in the folder
app/assets/javascripts/. Along this with this file, create two other
JavaScript files: game.js and game-driver.js. As you may have
noticed, the game.html file above loads the game-driver.js file.
Separating the driver code from the main JavaScript file in this way and use the Pedestal template system to load the file allows us to have some kind of driver which in the design view that will now be included in any other version of our application.
Update application.html to load the new JavaScript files by adding
the following script tags just before the <script id="script-driver"></script> tag at the bottom of the body section.
<script src="/raphael.js"></script>
<script src="/game.js"></script>We are now ready to begin.
Some initial design work has been done and the JavaScript API which defines the meeting place between application logic and rendering has been defined. The API is shown below as a collection of JavaScript functions.
Add these functions to the file app/assets/javascripts/game.js
// Create a new game object
var createGame = function(id) {
return BubbleGame(id);
}
// Add a player
var addPlayer = function(g, name) {
g.addPlayer(name);
}
// Add the handler function to call when a point is scored
var addHandler = function(g, f) {
g.addHandler(f);
}
// Set the score for a player
var setScore = function(g, n, x) {
g.setScore(n, x);
}
// Add a new bubble to the screen
var addBubble = function(g) {
g.addBubble();
}
// Remove a bubble from the screen
var removeBubble = function(g) {
g.removeBubble();
}
// Set the value of one of the game statistics
var setStat = function(g, n, v) {
g.setStat(n, v);
}
// The the order or position in the leaderboard for a player
var setOrder = function(g, n, i) {
g.setOrder(n, i);
}Throughout the rest of this section, we write the code that makes this API work.
The code for the game is shown below. We will add a brief comment for
each section. All of this code should be go into the file game.js.
Instead of just showing numbers on the screen, the total, maximum and average counter values as well as the dataflow statistics will be shown as to distinct bar charts. Each one with single bar that can show multiple values.
var Bar = function(paper, x, y, vals) {
var barAnimateTime = 2000;
var barHeight = 20;
var colors = ["#0f0", "#00f", "#f00"];
var rect = function(x, y, w, h, color) {
return paper.rect(x, y, w, h).attr({fill: color, stroke: "none"});
}
var bars = {};
for(var i in vals) {
var b = vals[i];
var size = b.size || 0;
b.bar = rect(x, y, size, barHeight, colors[i % colors.length]);
bars[b.name] = b;
}
var resizeBar = function(bar, size) {
bar.animate({width: size}, barAnimateTime);
}
return {
setSize: function(name, n) {
resizeBar(bars[name].bar, n);
},
vals: vals
}
}This object gives us the ability to set the size of a part of the bar if we know its name. It would be nice to be able to set the size by name of any part of multiple bars.
The code below takes an array of bars and provides this functionality.
var Bars = function(bars) {
var index = {};
for(var i in bars) {
var bar = bars[i];
var vals = bar.vals;
for(var j in vals) {
var val = vals[j];
index[val.name] = bar;
}
}
return {
setSize: function(name, n) {
var b = index[name];
if(b)
b.setSize(name, n);
}
}
}Instead of clicking a button to increment a counter the game will draw circles on the screen and move them around.
The code below handles the drawing and moving of circles.
var Circles = function(paper, w, h) {
var defaultRadius = 20;
var padding = 50;
var createAnimateTime = 500;
var removeAnimateTime = 200;
var moveAnimateTime = 1000;
var reportScoreFn;
var removeCounter = 0;
var randomPoint = function() {
var maxHeight = h - padding;
var x = Math.floor(Math.random() * w);
if(x < padding)
x = padding;
var y = Math.floor(Math.random() * h);
if(y < padding)
y = padding;
if(y > maxHeight)
y = maxHeight;
return {x: x, y: y};
}
var removeCircle = function(c) {
c.animate({r: 0}, removeAnimateTime, function() {
c.remove();
});
}
var moveCircle = function(c) {
if(c) {
var point = randomPoint();
c.animate({"cx": point.x, "cy": point.y}, moveAnimateTime, function() {
if(removeCounter > 0) {
c.animate({fill: "#000"}, 100)
removeCircle(c);
removeCounter--;
}
moveCircle(c);
});
}
}
var makeCircle = function() {
var point = randomPoint();
var circle = paper.circle(point.x, point.y, 0).attr({
fill: "#f00", stroke: "none", opacity: 0.6
});
circle.animate({r: defaultRadius}, createAnimateTime);
moveCircle(circle);
circle.mouseover(function() {
if(reportScoreFn)
reportScoreFn(1);
removeCircle(circle);
});
}
return {
addCircle: function() {
makeCircle();
},
removeCircle: function() {
removeCounter++;
},
addScoreReporter: function(f) {
reportScoreFn = f;
}
}
}The game will have a leaderboard showing the top scores. The code below handles drawing each name and score on the leaderboard.
var Player = function(paper, x, y, name) {
var nameLength = 150;
var fontSize = 20;
var score = 0;
var nameText = paper.text(x, y, name).attr({
"font-size": fontSize,
"text-anchor": "start"});
var scoreText = paper.text(x + nameLength, y, score).attr({
"font-size": fontSize,
"text-anchor": "end"});
var st = paper.set();
st.push(nameText, scoreText);
return {
setScore: function(n) {
score = n;
scoreText.attr({text: score});
},
moveTo: function(y) {
st.animate({y: y}, 400);
}
}
}And this code draws the leaderboard.
var Leaderboard = function(paper, x, y) {
var playerSpacing = 30;
var players = {};
var playerY = function(i) {
return 50 + (i * playerSpacing);
}
var countPlayers = function() {
var count = 0;
for(var i in players) {
if(players.hasOwnProperty(i))
count++;
}
return count;
}
return {
addPlayer: function(name) {
var i = countPlayers();
var p = Player(paper, x, playerY(i), name);
players[name] = p;
},
setScore: function(name, score) {
var p = players[name];
p.setScore(score);
},
setOrder: function(name, i) {
var p = players[name];
p.moveTo(playerY(i));
},
count: function() {
return countPlayers();
}
}
}The Bubble Game object pulls everything together. This code includes a loop that creates new bubbles every 2 seconds. This code will be removed once this being controlled from our application.
var BubbleGame = function(id) {
var paper = Raphael(id, 800, 400);
var bars = Bars([Bar(paper, 0, 380, [{name: "total-count"},
{name: "max-count"},
{name: "average-count"}]),
Bar(paper, 0, 357, [{name: "dataflow-time-max"},
{name: "dataflow-time-avg"},
{name: "dataflow-time"}])]);
var circles = Circles(paper, 500, 380);
var leaderboard = Leaderboard(paper, 550, 0);
// This will be removed as we make improvements to the game.
// The dataflow will control when circles are created.
var makeCircles = function() {
var p = leaderboard.count();
for(var i=0;i<p;i++) {
circles.addCircle();
}
}
setInterval(makeCircles, 2000);
return {
addHandler: circles.addScoreReporter,
addPlayer: leaderboard.addPlayer,
setScore: leaderboard.setScore,
setOrder: leaderboard.setOrder,
setStat: bars.setSize,
addBubble: circles.addCircle,
removeBubble: circles.removeCircle
}
}While working on the code above, we also created a JavaScript driver
in the file app/assets/javascripts/game-driver.js. The driver
simulates all of the activities of the application.
The driver works so well that it allows us to play the game. Creating a driver like this is a great way to quickly iterate on the drawing code.
var game = BubbleGame("game-board");
var me = {name: "Me", score: 0};
var players = [me,
{name: "Fred", score: 0},
{name: "ahbhgtre", score: 0}];
var sortPlayers = function() {
players.sort(function(a, b) {
if(a.score < b.score) return 1;
if(a.score > b.score) return -1;
return 0;
});
for(var i=0;i<players.length;i++) {
players[i].newIndex = i;
}
}
for(var i in players) {
game.addPlayer(players[i].name);
}
var updateCounts = function() {
var total = 0;
var max = 0;
for(var i in players) {
var score = players[i].score;
total += score;
if(score > max)
max = score;
}
var avg = total / players.length;
game.setStat("total-count", total);
game.setStat("max-count", max);
game.setStat("average-count", avg);
}
setInterval(updateCounts, 1000);
game.addHandler(function(points) {
me.score += points;
game.setScore("Me", me.score);
updateCounts();
});
var rand = function(n) {
return Math.floor(Math.random() * n) + n;
}
var randPlayer = function() {
return Math.floor(Math.random() * players.length);
}
var updateDataflowStats = function() {
game.setStat("dataflow-time-max", rand(100));
game.setStat("dataflow-time-avg", rand(50));
game.setStat("dataflow-time", rand(10));
}
setInterval(updateDataflowStats, 1000);
var updatePlayerScores = function() {
var p = players[randPlayer()];
if(p.name != "Me") {
p.score += 1;
game.setScore(p.name, p.score);
game.removeBubble();
}
updateCounts();
}
setInterval(updatePlayerScores, 1000);
var updatePlayerOrder = function() {
sortPlayers();
for(var i in players) {
var p = players[i];
game.setOrder(p.name, i);
}
}
setInterval(updatePlayerOrder, 2000);With all of this in place, we can go to the design page
http://localhost:3000/design.html
click the Design link and experiment with the game. We will come back to the page later as we make improvements.
We now have all of the drawing code in place. In the next section we will update our renderer to use this for drawing instead of an HTML template.
The tag for this step is step11.