What's up, you glorious geeks? Easter approaches!
Traditionally, at this time of year, we have our Easter-themed web tutorial. This year, I shall not disappoint... and there's a special treat because I will be using VueJS for this one.
My usage of the framework will be a very rudimentary one, just to get beginners off on the right foot. What we will be doing is a very simple puzzle game. We will use an Easter-themed image, like this one. It's a 500 by 500 pixels square image.
 |
| easter.jpg |
Now imagine if the image was scrambled and you had to unscramble it within a time limit. That will be what our game does!
The setup
As usual, it all begins with some HTML.
<!DOCTYPE html>
<html>
<head>
<title>Easter Puzzle</title>
<style>
</style>
</head>
<body>
<script>
</script>
</body>
</html>
Here, let's add something to the styling - a
red outline to all divs for dev purposes.
<!DOCTYPE html>
<html>
<head>
<title>Easter Puzzle</title>
<style>
div { outline: 0px solid #FF0000;}
</style>
</head>
<body>
<script>
</script>
</body>
</html>
The next few items are specific to VueJS. We need a div with an id of
easterApp. And then we need to link the VueJS library. Finally, we begin scripting by declaring a variable
app, and setting it to a new instance of the
Vue object.
<html>
<head>
<title>Easter Puzzle</title>
<style>
div { outline: 1px solid #FF0000;}
</style>
</head>
<body>
<div id="easterApp">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.18/vue.min.js"></script>
<script>
var app = new Vue
(
);
</script>
</body>
</html>
We then fill in
app with an object within the new
Vue object. It will have the properties
el (which we will set to the id
easterApp to let Vue know which DOM element to reference),
methods to hold the different methods for this app, and
created to define what gets run as soon as the app loads.
<script>
var app = new Vue
(
{
el: "#easterApp",
data:
{
},
methods:
{
},
created: function()
{
}
}
);
</script>
Inside
easterApp, add a div with an id of
timeContainer. Inside that, put some HTML. These are elements that will be populated later. And then we'll also have a div with an id of
puzzleContainer right under
timeContainer.
<div id="easterApp">
<div id="timeContainer">
<h2>Message</h2>
<h1>Seconds</h1>
<button>ButtonText</button>
</div>
<div id="puzzleContainer">
</div>
</div>
Let's style!
easterApp will be 500 pixels wide. We'll use the
margin property to set it center of the screen, and then set some font styling.
<style>
div { outline: 1px solid #FF0000;}
#easterApp
{
width: 500px;
margin: 0 auto 0 auto;
font-family: verdana;
font-size: 12px;
}
</style>
Next is
timeContainer. It will take up full width of
easterApp. We give it 150 pixels in height, and align text center.
<style>
div { outline: 1px solid #FF0000;}
#easterApp
{
width: 500px;
margin: 0 auto 0 auto;
font-family: verdana;
font-size: 12px;
}
#timeContainer
{
width: 100%;
height: 150px;
text-align: center;
}
</style>
Here, I'm going to style the button. Use whatever fancy CSS you like, but I'm gonna go with
orange background and
white text.
<style>
div { outline: 1px solid #FF0000;}
#easterApp
{
width: 500px;
margin: 0 auto 0 auto;
font-family: verdana;
font-size: 12px;
}
#timeContainer
{
width: 100%;
height: 150px;
text-align: center;
}
#timeContainer button
{
width: 8em;
height: 2em;
text-align: center;
background-color: rgba(255, 100, 0, 1);
color: rgba(255, 255, 255, 1);
font-weight: bold;
border-radius: 10px;
border: 0px solid black;
}
#timeContainer button:hover
{
background-color: rgba(255, 100, 0, 0.5);
color: rgba(255, 0, 0, 1);
}
</style>
And
puzzleContainer will be a square, so we give it a height of 500 pixels.
<style>
div { outline: 1px solid #FF0000;}
#easterApp
{
width: 500px;
margin: 0 auto 0 auto;
font-family: verdana;
font-size: 12px;
}
#timeContainer
{
width: 100%;
height: 150px;
text-align: center;
}
#timeContainer button
{
width: 8em;
height: 2em;
text-align: center;
background-color: rgba(255, 100, 0, 1);
color: rgba(255, 255, 255, 1);
font-weight: bold;
border-radius: 10px;
border: 0px solid black;
}
#timeContainer button:hover
{
background-color: rgba(255, 100, 0, 0.5);
color: rgba(255, 0, 0, 1);
}
#puzzleContainer
{
width: 100%;
height: 500px;
}
</style>
This looks like a good start, wouldn't you say?
In the
data object, fill in these properties.
timer is set to
undefined by default. We have
seconds with a value of 100 (which is perhaps a bit too much, but you can tweak this later) and
btnText, whose default value is "RESET". For
message, set it to "Time elapsed".
pieces will be an empty array.
data:
{
timer: undefined,
seconds: 100,
btnText: "RESET",
message: "Time elapsed",
pieces: []
},
Within the
created() method, make a call to the
reset() method. This means that as soon as the app loads,
reset() will run.
created: function()
{
this.reset();
}
You'll notice that
reset() is prefixed by a reference to
this. That's because
reset() is a method within the same object that
created() is a child of. We will create
reset() now, under
methods.
methods:
{
reset: function()
{
},
},
In here, we set the properties in
data to their default values.
methods:
{
reset: function()
{
this.seconds = 100;
this.btnText = "RESET";
this.message = "Time elapsed";
this.pieces = [];
},
},
Then we insert calls to
stopTimer() and
startTimer().
methods:
{
reset: function()
{
this.stopTimer();
this.seconds = 100;
this.btnText = "RESET";
this.message = "Time elapsed";
this.startTimer();
this.pieces = [];
},
},
Next, we create these methods.
reset: function()
{
this.stopTimer();
this.seconds = 100;
this.btnText = "RESET";
this.message = "Time elapsed";
this.startTimer();
this.pieces = [];
},
startTimer: function()
{
},
stopTimer: function()
{
},
For
stopTimer(), we use
timer as an argument in the
clearInterval() function. This means, basically, that we stop the timer, and then we set
timer to
undefined.
stopTimer: function()
{
clearInterval(this.timer);
this.timer = undefined;
},
For
startTimer(), this only fires off if
timer is
undefined. And it would be if
stopTimer() was run.
startTimer: function()
{
if (this.timer == undefined)
{
}
},
If
timer is
undefined, set
timer by running
setInterval with an interval of 1 second.
startTimer: function()
{
if (this.timer == undefined)
{
this.timer = setInterval
(
() =>
{
},
1000
);
}
},
In here, decrement
seconds.
startTimer: function()
{
if (this.timer == undefined)
{
this.timer = setInterval
(
() =>
{
this.seconds = this.seconds - 1;
},
1000
);
}
},
Now check for the value of
seconds. If it's reached the value of 0, run
stopTimer(). Then set
btnText to "REPLAY" and set
message.
startTimer: function()
{
if (this.timer == undefined)
{
this.timer = setInterval
(
() =>
{
this.seconds = this.seconds - 1;
if (this.seconds == 0)
{
this.stopTimer();
this.btnText = "REPLAY";
this.message = "Better luck next time!";
}
},
1000
);
}
},
Now in the HTML, replace the text we put there, with template strings.
<div id="timeContainer">
<h2>{{message}}</h2>
<h1>{{seconds}}</h1>
<button>{{btnText}}</button>
</div>
Add a click event handler using
v-on, and make it call
reset().
<div id="timeContainer">
<h2>{{message}}</h2>
<h1>{{seconds}}</h1>
<button v-on:click="reset">{{btnText}}</button>
</div>
Now you'll see that there's a number there where "seconds" was, and it's counting down each second! Also, there's a "REPLAY" button now. If you click it,
reset() will be called and the seconds display will go back to 100, then start counting down again.
And once it reaches 0, the message changes!
Next, we're going to populate the puzzle. It will be a 5 by 5 square grid. So create a nested
For loop like this. 5 rows, 5 columns.
reset: function()
{
this.stopTimer();
this.seconds = 100;
this.btnText = "RESET";
this.message = "Time elapsed";
this.startTimer();
this.pieces = [];
for (var row = 0; row < 5; row++)
{
for (var col = 0; col < 5; col++)
{
}
}
},
Because each square is going to be 25 by 25 pixels, declare
offsetX as the product of
col by 25.
offsetY will be the product of
row by 25.
for (var row = 0; row < 5; row++)
{
for (var col = 0; col < 5; col++)
{
var offsetX = col * 25;
var offsetY = row * 25;
}
}
Declare a temporary object,
piece. The
id property will be unique and going by that formula, it will be from 0 to 24. Set the
rotation property to 0, for now. And then set the
offsetX and
offsetY properties to the current value of
offsetX and
offsetY, respectively.
for (var row = 0; row < 5; row++)
{
for (var col = 0; col < 5; col++)
{
var offsetX = col * 25;
var offsetY = row * 25;
var piece =
{
id: (row * 5) + col,
rotation: 0,
offsetX: offsetX,
offsetY: offsetY
}
}
}
And finally, push
piece into the
pieces array. So by the time the nested
For loop is done, you should have 25 elements in the
pieces array, each with a different
offsetX and
offsetY value.
for (var row = 0; row < 5; row++)
{
for (var col = 0; col < 5; col++)
{
var offsetX = col * 25;
var offsetY = row * 25;
var piece =
{
id: (row * 5) + col,
rotation: 0,
offsetX: offsetX,
offsetY: offsetY
}
this.pieces.push(piece);
}
}
In the HTML, inside
puzzleContainer, create a div with a class of
pieceContainer, and use
v-for to ensure that this repeats for every element in
pieces.
<div id="puzzleContainer">
<div class="pieceContainer" v-for="piece in pieces">
</div>
</div>
Now in the CSS, add the
pieceContainer CSS class. The width and height will be 20% of
puzzleContainer, which is 25 pixels. And we set the
float property to
left.
#puzzleContainer
{
width: 100%;
height: 500px;
}
.pieceContainer
{
width: 20%;
height: 20%;
float: left;
}
At this point, you should have a 5 by 5 square grid, each square 25 pixels in height and width.
Back to the HTML, within each div styled using
pieceContainer, insert a div styled using
piece.
<div id="puzzleContainer">
<div class="pieceContainer" v-for="piece in pieces">
<div class="piece">
</div>
</div>
</div>
Use
v-bind to set the
data-id property to the
id property of
piece.
<div id="puzzleContainer">
<div class="pieceContainer" v-for="piece in pieces">
<div class="piece" v-bind:data-id="piece.id">
</div>
</div>
</div>
Let's add something to the CSS - the
piece class. Each of these will fill full width and height of its parent, and its background image will be
easter.jpg.
.pieceContainer
{
width: 20%;
height: 20%;
float: left;
}
.piece
{
width: 100%;
height: 100%;
background-image: url(easter.jpg);
background-repeat: no-repeat;
}
And there it is...
Now use
v-bind on the
style attribute. The value will be what's returned from the
getStyle() method with
piece passed in as an argument. We'll create that next.
<div id="puzzleContainer">
<div class="pieceContainer" v-for="piece in pieces">
<div class="piece" v-bind:data-id="piece.id" v-bind:style="getStyle(piece)">
</div>
</div>
</div>
Inside the
methods object, create the
getStyle() method. It will accept
obj as a parameter.
stopTimer: function()
{
clearInterval(this.timer);
this.timer = undefined;
},
getStyle: function(obj)
{
},
In here, we will generate CSS. Declare
rotate and set it to some CSS code. We use the
rotation property (which is currently 0 for all pieces).
getStyle: function(obj)
{
var rotate = "transform: rotate(" + obj.rotation + "deg)";
},
Then we declare
offset and do something similar, only this time it's for the
background-position property, and we use the
offsetX and
offsetY properties of
obj to generate the CSS.
getStyle: function(obj)
{
var rotate = "transform: rotate(" + obj.rotation + "deg)";
var offset = "background-position:" + obj.offsetX + "% " + obj.offsetY + "%";
},
And finally, we return the full CSS style string.
getStyle: function(obj)
{
var rotate = "transform: rotate(" + obj.rotation + "deg)";
var offset = "background-position:" + obj.offsetX + "% " + obj.offsetY + "%";
return rotate + ";" + offset;
},
And now we have a full picture.
Let's randomly rotate all the pieces now. Get back to the
reset() method. Inside the inner loop of the nested
For loop, declare
randomRotation. There are 4 possibilities - from 0 to 3. Then multiply by 90 to get the degree of rotation. Finally, change
rotation within the temporary
piece object to
randomRotation.
for (var row = 0; row < 5; row++)
{
for (var col = 0; col < 5; col++)
{
var randomRotation = Math.floor(Math.random() * 3) * 90;
var offsetX = col * 25;
var offsetY = row * 25;
var piece =
{
id: (row * 5) + col,
rotation: randomRotation,
offsetX: offsetX,
offsetY: offsetY
}
this.pieces.push(piece);
}
}
Now this is what it should look like, with pieces all rotated.
Sweet! Now we will add something for the user to be able to rotate the pieces on their own. Use
v-on to add a click event, and set it to call the
rotatePiece() method.
<div class="piece" v-bind:data-id="piece.id" v-bind:style="getStyle(piece)" v-on:click="rotatePiece">
</div>
Obviously, we will then add this method under
methods.
getStyle: function(obj)
{
var rotate = "transform: rotate(" + obj.rotation + "deg)";
var offset = "background-position:" + obj.offsetX + "% " + obj.offsetY + "%";
return rotate + ";" + offset;
},
rotatePiece: function(e)
{
},
This will only fire off if
timer is
undefined, which means the game is in progress.
rotatePiece: function(e)
{
if (this.timer != undefined)
{
}
},
Declare
piece. Set it to the element in
pieces pointed to by the
id property in the event's element. Remember we set the
data-id property? Yep, so we're going to make some changes to
piece, then reassign the value back to the element of the
pieces array.
rotatePiece: function(e)
{
if (this.timer != undefined)
{
var piece = this.pieces[e.target.dataset.id];
this.pieces[piece.id] = piece;
}
},
Now bear in mind that we can only rotate 90 degrees clockwise. That will be a limit we set. Use an
If-else block. Check if the
rotation property is 270.
rotatePiece: function(e)
{
if (this.timer != undefined)
{
var piece = this.pieces[e.target.dataset.id];
if (piece.rotation == 270)
{
}
else
{
}
this.pieces[piece.id] = piece;
}
},
So if the rotation is at 270 degrees, one more rotation would put it back at 0 degrees. Otherwise, just add 90 to the current value of the
rotation property.
rotatePiece: function(e)
{
if (this.timer != undefined)
{
var piece = this.pieces[e.target.dataset.id];
if (piece.rotation == 270)
{
piece.rotation = 0;
}
else
{
piece.rotation = piece.rotation + 90;
}
this.pieces[piece.id] = piece;
}
},
Now add a new method -
checkIncorrectPieces().
rotatePiece: function(e)
{
if (this.timer != undefined)
{
var piece = this.pieces[e.target.dataset.id];
if (piece.rotation == 270)
{
piece.rotation = 0;
}
else
{
piece.rotation = piece.rotation + 90;
}
this.pieces[piece.id] = piece;
}
},
checkIncorrectPieces: function()
{
}
Basically, we declare
incorrect, then set it to a new array derived from running
pieces through the
filter() method, returning all those elements where the
rotation property is not 0. Because 0 is the correct value. After that, we return the number of incorrect values.
checkIncorrectPieces: function()
{
var incorrect = this.pieces.filter((x) => { return x.rotation != 0;});
return incorrect.length;
}
So every time the timer runs, it should check if the number of incorrect pieces is 0, which means that all the pieces have been correctly rotated. When that happens, we run the
stopTimer() method, set
btnText and
message, and this effectively ends the game.
startTimer: function()
{
if (this.timer == undefined)
{
this.timer = setInterval
(
() =>
{
this.seconds = this.seconds - 1;
if (this.seconds == 0)
{
this.stopTimer();
this.btnText = "REPLAY";
this.message = "Better luck next time!";
}
if (this.checkIncorrectPieces() == 0)
{
this.stopTimer();
this.btnText = "REPLAY";
this.message = "Congratulations!";
}
},
1000
);
}
},
Now when you rotate all the pieces correctly, the timer stops moving and you get a congratulatory message!
A few final touches!
Add this to the CSS so that the animation we're about to add, comes out nicely.
#puzzleContainer
{
width: 100%;
height: 500px;
transition: all 1s;
}
Bind the class attribute using
v-bind. If
checkIncorrectPieces() is 0, then the class is
win.
<div id="puzzleContainer" v-bind:class="checkIncorrectPieces() == 0 ? 'win' : ''">
<div class="pieceContainer" v-for="piece in pieces">
<div class="piece" v-bind:data-id="piece.id" v-bind:style="getStyle(piece)" v-on:click="rotatePiece">
</div>
</div>
</div>
Add
win to the CSS. It will be a nice thick
orange outline.
.piece
{
width: 100%;
height: 100%;
background-image: url(easter.jpg);
background-repeat: no-repeat;
}
.win
{
outline: 5px solid rgba(255, 100, 0, 1);
}
Now when you win the game, you get this
orange outline fading in! If you click the REPLAY button, it should fade out again.
Finally, remove the
red outline.
div { outline: 0px solid #FF0000;}
And this is your final product.
Final Notes
I do like VueJS, or at least, I like the version we're using and how we're using it. The structure is pretty much how I usually write my JavaScript anyway, and it combines the best features of AngularJS and ReactJS without being a pain in the ass about it.
Piece be with you!
T___T