Professional Documents
Culture Documents
Table of Contents
1. Introduction.................................................................................................................... 3
What is Meteor Streams.......................................................................................................... 3
What's in this eBook................................................................................................................. 3
Source Code................................................................................................................................. 3
Thank You.......................................................................................................................... 19
1. Introduction
I hope you have tried the Realtime Blackboard application that I've created. If not,
please watch this video and try it yourself. As the name implies, it is a realtime
blackboard where multiple users can draw and share on a common blackboard.
based security model, and through filters, you can control what clients are
communicating.
To follow this eBook, you don't need prior experience with Meteor Streams. But try
having a look at Meteor Stream documentation before you proceed.
Source Code
Source code for the application we are creating is available on the Github. Click
here to get it.
At the end of each chapter, you can view and download the source code with the
changes made in that chapter and previous chapters.
So why wait? Lets get started!
With Atmosphere
Atmosphere is a community-managed Smart Package repository. You can install
atmosphere packages with meteorite. Once you've installed meteorite, installing
Meteor Streams is just a single shell command.
Using Git
If you are not a fan of meteorite and atmosphere, you can add Meteor Streams to
your app as a git submodule.
Now you've added Meteor Streams support for your app. Easy, wasnt it?
Adding hammer.js
Our app needs to work smoothly on every possible browser, including mobile
browsers. We need some help from a third party-library. We'll use hammer.js to
support touch events. Let's add it to our project:
body {
margin: 0;
overflow: hidden;
background-color: black;
}
#header {
position: fixed;
top: 0px;
left: 0px;
color: rgb(220, 220, 220);
border-bottom: 1px solid rgb(100, 100, 100);
padding: 15px 5px 5px 10px;
width: 100%;
}
#header #heading {
float: left;
width: 300px;
}
#header #controls {
float: right;
width: 600px;
text-align: right;
padding-right: 20px;
}
#header #heading h1 {
font-size: 28px;
line-height: 25px;
margin: 0px 0px 8px 0px;
float: left;
}
#header #heading h2 {
margin-bottom: 10px;
font-size: 16px;
line-height: 16px;
font-weight: normal;
}
#header #heading h2 a {
color: inherit;
text-decoration: none;
font-weight: bold;
border-bottom: 1px dashed white;
}
.nickname {
position: absolute;
font-family: 'Arial';
padding: 1px 4px 1px 4px;
font-size: 13px;
border-radius: 3px;
border: 2px solid rgb(100, 100, 100);
color: white;
background-color: black;
}
We have a single template named pad that the blackboard is rendered into. Add the
following content to client/view/pad.html
<template name='pad'>
<canvas></canvas>
<div id='header'>
<div id='heading'>
<h1>Realtime Blackboard</h1>
</div>
<div id='controls'>
<span id='show-nickname'>Hello, <b>user</b></span>
<input class='btn btn-info btn-small' type="button"
id='set-nickname' value='Change Nickname'/>
<input class='btn btn-info btn-small' type="button"
id='wipe' value='Clear Backboard'/>
<input class='btn btn-success btn-small' type="button"
id='create-new' value='Create New'/>
</div>
</div>
<script id='tmpl-nickname' type="text/html">
<span class='nickname'>
nickname
</span>
</script>
</template>
Now that we've added all the client side static files we need, its time to focus on the
routing.
Adding Routes
Add following content as client/routes.js
Meteor.Router.add({
'/': function() {
var newPadId = Random.id();
location.href = '/' + newPadId;
},
'/:padId': {
as: 'pad',
to: function(padId) {
Session.set('padId', padId);
return 'pad'
}
}
});
The first route is simple. It will create a new padId and redirect to the second route.
The second route is a named route, named pad. It will set the padId, given in the
URL, to the Session, where the rest of our app can read it reactively.
10
if(!Meteor.isClient) return;
this.Pad = function Pad(id) {
var canvas = $('canvas');
var ctx = canvas[0].getContext('2d');
var drawing = false;
var from;
var skipCount = 0;
var nickname;
var color;
setNickname(localStorage.getItem('nickname') || Random.id());
var pad = canvas.attr({
width: $(window).width(),
height: $(window).height()
}).hammer()
//hammer.js touch events
pad.on('dragstart', onDragStart);
pad.on('dragend', onDragEnd);
pad.on('drag', onDrag);
function onDrag(event) {
if(drawing) {
var to = getPosition(event);
drawLine(from, to, color);
from = to;
skipCount = 0;
}
}
function onDragStart(event) {
drawing = true;
from = getPosition(event);
}
function onDragEnd() {
11
drawing = false;
}
function getPosition(event) {
return {
x: parseInt(event.gesture.center.pageX),
y: parseInt(event.gesture.center.pageY)
};
}
function drawLine(from, to, color) {
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.closePath();
ctx.stroke();
}
function setNickname(name) {
nickname = name;
$('#show-nickname b').text(nickname);
localStorage.setItem('nickname', nickname);
color = localStorage.getItem('color-' + nickname);
if(!color) {
color = getRandomColor();
localStorage.setItem('color-' + nickname, color);
}
}
function wipe(emitAlso) {
ctx.fillRect(0, 0, canvas.width(), canvas.height());
}
ctx.strokeStyle = color;
ctx.fillStyle = '#000000';
ctx.lineCap = 'round';
ctx.lineWidth = 3;
ctx.fillRect(0, 0, canvas.width(), canvas.height());
// Stop iOS from doing the bounce thing with the screen
document.ontouchmove = function(event){
event.preventDefault();
}
//expose API
this.drawLine = drawLine;
this.wipe = wipe;
this.setNickname = setNickname;
this.close = function() {
pad.off('dragstart', onDragStart);
pad.off('dragend', onDragEnd);
pad.off('drag', onDrag);
};
12
function getRandomColor() {
var letters = '0123456789ABCDEF'.split('');
var color = '#';
for (var i = 0; i < 6; i++ ) {
color += letters[Math.round(Math.random() * 15)];
}
return color;
}
Now it is time to integrate the above Pad class with our routes. To do that, add
following content into client/views/pad.js
var pad;
Meteor.startup(function() {
Deps.autorun(function() {
if(pad) {
pad.close();
}
var padId = Session.get('padId');
pad = new Pad(padId);
});
});
Whenever the router sets a padId, the above code will take care of closing the existing
pad and create a new one.
Have you noticed the 3 buttons we have in the right top corner? Let's add their
functionality.
Append the following content to client/views/pad.js.
$(function() {
//Clear Blackboard
$('body').on('click', '#wipe', function() {
pad.wipe(true);
});
//Change Nickname
$('body').on('click', '#set-nickname', function() {
var name = prompt('Enter your nickname');
if(name && name.trim() != '') {
pad.setNickname(name);
}
});
13
//Create New
$('body').on('click', '#create-new', function() {
var newPadId = Random.id();
Meteor.Router.to('pad', newPadId);
});
});
In the next section you'll be starting to integrate Meteor Streams with the
Blackboard and make it realtime.
View Source Code
14
If you don't have a working blackboard app (maybe you've decided to directly jump
into this chapter), download it from here.
Meteor.Stream is the class exposed by Meteor Streams, which allows you to create a
Stream. It is a realtime EventEmitter and works across the Meteor. You can pass
messages between clients. You can even send and listen to the messages on the
Server.
Location where the mouse dragging (or touching on the screen) started
Locations the user is currently dragging
Location where the dragging has ended
Nickname and the color of the pencil
We'll use these three events to communication and listen to the above information.
But we have multiple pads in a single app, so we need to namespace the above events
with the padId as shown below. Let's assume our padId is L4nN7r8FpJ6xgpEiJ.
15
L4nN7r8FpJ6xgpEiJ:dragstart
L4nN7r8FpJ6xgpEiJ:drag
L4nN7r8FpJ6xgpEiJ:dragend
Permissions
By default, clients don't have read or write access to the Stream. We need to explicitly
enable permissions. We can use event name, userId and subscriptionId to make
the decision to allow or deny conditionally.
For simplicity, we'll enable clients to communicate without any restrictions at first.
But in the later chapters, we'll discuss more on advanced permissions, which make
the app more efficient.
Let's start integrating Meteor Streams!
16
It will create a Stream in both client and server and add permissions. Here we simply
add no restrictions to the stream. Its not ideal, but we'll catch up with this later.
if(drawing) {
var to = getPosition(event);
drawLine(from, to, color);
+
LineStream.emit(id + ':drag', nickname, to);
from = to;
skipCount = 0;
}
@@ -33,10 +33,12 @@ this.Pad = function Pad(id) {
function onDragStart(event) {
drawing = true;
from = getPosition(event);
+
LineStream.emit(id + ':dragstart', nickname, from, color);
}
17
function onDragEnd() {
drawing = false;
+
LineStream.emit(id + ':dragend', nickname);
}
function getPosition(event) {
@@ -66,6 +68,9 @@ this.Pad = function Pad(id) {
+
+
+
function wipe(emitAlso) {
ctx.fillRect(0, 0, canvas.width(), canvas.height());
if(emitAlso) {
LineStream.emit(id + ':wipe', nickname);
}
Click here to view the full file if you are having difficulty understanding the above
diff.
Now we are emitting events, so the next step is to listen to those events and render
them onto the screen. To do that, I've created a class called RemotePad. I've also
added inline comments for you to understand it quickly.
Add the following content to lib/remote_pad.js
if(!Meteor.isClient) return;
this.RemotePad = function RemotePad(padId, pad) {
var users = {};
//listening on the dragstart event for the given padId
LineStream.on(padId + ':dragstart', function(nickname, position,
color) {
//display the nickname pointer on the screen as remote user draws
on the pad
var pointer = $($('#tmpl-nickname').text());
pointer.text(nickname);
positionPointer(pointer, position);
$('body').append(pointer);
users[nickname] = {
color: color,
from: position,
pointer: pointer
};
});
18
19
As the last step, we need to integrate RemotePad when we are creating a route. See
following diff of the client/views/pad.js for how you can do it.
@@ -1,13 +1,16 @@
var pad;
+var remotePad;
Meteor.startup(function() {
Deps.autorun(function() {
if(pad) {
pad.close();
+
remotePad.close();
}
var padId = Session.get('padId');
pad = new Pad(padId);
+
remotePad = new RemotePad(padId, pad);
});
});
Click here to view the full file if you are having difficulty understanding the above
diff.
Okay, now our blackboard is realtime and multiple users can draw on it at the same
time. Nice! To see that for yourself, open the same blackboard URL in two different
browsers and see.
View Source Code
20
In the next section, you'll see that I've discussed subscription and
subscriptionId. A subscription is created when a client (browser tab) is connected
to a stream. Meteor Streams creates a unique id for each subscription and it is the
subscriptionId
21
Then we need to listen to the pad event on the server and update our permissions. To
do that, you need to replace streams.js with the following content. I've added
inline comments to make it more understandable.
Hooray! Now our blackboard is fully functional and efficient. Deploy it and try with
your friends in realtime.
View Source Code
22
Thank You
Thank you for taking the time to read this eBook. Ive tried to make it simple and
straight to the point. I hope you have enjoyed it.
Try to build something innovative using Meteor Streams. Try to use it with your next
project. Meteor Streams is a new project, but I will continue to develop it -- theres a
lot to do.
If you encounter something weird with Meteor Streams, create an issue for it. And I
would love to see your contributions. It could be a blog post, sample app, an issue or a
pull request.
Have a Good Day!
Arunoda Susiripala
@arunoda
http://meteorhacks.com
23