Chapter 11
In the browser
11.1 Getting started
Listing 11.1 The basic HTML page
<!DOCTYPE html>
<html dir='ltr' lang='en-US'>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<title>Radiator</title>
<style type="text/css">
html, body { padding: 0; margin:0; }
</style>
<script src='status.js'></script>
</head>
<body>
<div id='status'></div>
</body>
</html>
> coffee ?c status.coffee
<script src='http://coffeescript.org/extras/coffee-script.js'></script>
<script type='text/coffeescript' src='status.coffee'></script>
> coffee ?c ?w status.coffee
# 21:52:16 - compiled status.coffee
# 21:52:57 - compiled status.coffee
# 21:53:00 - compiled status.coffee
11.2 Communicating with the outside world
http = (method, src, callback) ->
handler = ->
if @readyState is 4 and @status is 200
unless @responseText is null
callback JSON.parse @responseText
client = new XMLHttpRequest
client.onreadystatechange = handler
client.open method, src
client.send()
get = (src, callback) ->
http "GET", src, callback
window.serverStatusCallback = (status) ->
console.log status
head = document.querySelector 'head'
script = document.createElement 'script'
script.src = 'http://www.agtronsapi.com/server-status.js'
head.appendChild script
window.serverStatusCallback({
'server 1': {
'cpu': 22,
'network': {
'in': 2343,
'out' 3344
}
}
});
Listing 11.2 The status updating script
window.onload = ->
status = document.querySelector '#status'
render = (buffer) ->
status.style.color = 'green'
status.style.fontSize = '120px'
status.innerHTML = buffer[buffer.length-1]
nextCallbackId = do ->
callbackId = 0
-> callbackId = callbackId + 1
nextCallbackName = ->
"callback#{nextCallbackId()}"
fetch = (src, callback) ->
head = document.querySelector 'head'
script = document.createElement 'script'
ajaxCallbackName = nextCallbackName()
window[ajaxCallbackName] = (data) ->
callback data
script.src = src + "?callback=#{ajaxCallbackName}"
head.appendChild script
seconds = (n) ->
1000*n
framesPerSecond = (n) ->
(seconds 1)/n
makeUpdater = (buffer = []) ->
bufferRenderer = (json) ->
buffer.push (JSON.parse json).hits
render buffer
->
window.setInterval ->
fetch '/feed.json', bufferRenderer
, framesPerSecond 20
updater = makeUpdater()
updater()
{
'servers': [
{
'name': 'tolimas'
'cpu': 22,
'network': {
'in': 2343,
'out' 3344
}
}
]
}
Array::map = -> null
[9,8,7,6].map (item) -> item*2
# null
Listing 11.3 A random-number socket emitter
{EventEmitter} = require 'events'
WebSocketServer = (require 'websocket').server
seconds = (n) -> n*1000
emitRandomNumbers = (emitter, event, interval) ->
setInterval ->
emitter.emit event, Math.floor Math.random()*100
, interval
source = new EventEmitter
emitRandomNumbers source, 'update', seconds(4)
attachSocketServer = (server) ->
socketServer = new WebSocketServer httpServer: server
socketServer.on 'request', (request) ->
connection = request.accept 'graph', request.origin
source.on 'update', (data) ->
connection.sendUTF JSON.stringify data
exports.attachSocketServer = attachSocketServer
socket = new WebSocket 'ws://www.agtronsapi.com/server-data-socket'
socket.onmessage = (message) -> console.log "Received message #{message}"
11.3 Cross-browser compatibility
document.querySelector '#status'
# document.querySelector is not a function
document?
# false
document.querySelector ?= (selector) ->
(document.getElementById (selector.replace /^#/gi, ''))
<ul class='pages'>
<li></li>
<li class='active'>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
document.querySelector '.links .active'
# null
document.querySelector ?= (selector) ->
if /^#/.test selector
(document.getElementById (selector.replace /^#/gi, ''))
else
throw new Error 'Not supported by this implementation'
Object.create ?= (prototype) ->
F = ->
F.prototype = prototype
new F()
Object.create ?= (prototype, extensions) ->
if extensions
throw new Error 'Not supported by this implementation'
else
F = ->
F.prototype = prototype
new F()
11.4 Creating a user interface
number = document.querySelector '#status'
number.innerHTML = 55
number.innerHTML
# 55
for number, index in values
measurement[index].style.height = 55
Listing 11.4 Drawing a bar chart with DOM elements
window.onload = ->
status = document.querySelector '#status'
ensureBars = (number) ->
unless (document.querySelectorAll '.bar').length >= number
for n in [0..number]
bar = document.createElement 'div'
bar.className = 'bar'
bar.style.width = '60px'
bar.style.position = 'absolute'
bar.style.bottom = '0'
bar.style.background = 'green'
bar.style.color = 'white'
bar.style.left = "#{60*n}px"
status.appendChild bar
render = (buffer) ->
ensureBars 20
bars = document.querySelectorAll '.bar'
for bar, index in bars
bar.style.height = "#{buffer[index]}px"
bar.innerHTML = buffer[index] || 0
nextCallbackId = do ->
callbackId = 0
-> callbackId = callbackId + 1
nextCallbackName = ->
"callback#{nextCallbackId()}"
fetch = (src, callback) ->
head = document.querySelector 'head'
script = document.createElement 'script'
ajaxCallbackName = nextCallbackName()
window[ajaxCallbackName] = (data) ->
callback data
script.src = src + "?callback=#{ajaxCallbackName}"
head.appendChild script
seconds = (n) ->
1000*n
framesPerSecond = (n) ->
(seconds 1)/n
makeUpdater = (buffer = []) ->
bufferRenderer = (json) ->
buffer.push (JSON.parse json).hits
if buffer.length is 22 then buffer.shift()
render buffer
->
window.setInterval ->
fetch '/feed.json', bufferRenderer
, framesPerSecond 1
updater = makeUpdater()
updater()
graph = document.createElement 'canvas'
graph.width = '800'
graph.height = '600'
graph.id = 'graph'
document.querySelector('#status').appendChild graph
context = canvas.getContext '2d'
context.beginPath()
context.lineTo 0,0
context.lineTo 1,10
context.stroke()
createGraph = (element) ->
graph = document.createElement 'canvas'
graph.width = '800'
graph.height = '600'
graph.id = 'graph'
element.appendChild graph
graph
getClearedContext = (element) ->
element.width = element.width
element.getContext()
drawLineGraph = (element, graphData, horizontalScale) ->
context = element.
context.beginPath()
for y, x in graphData
context.lineTo x*horizontalScale, y
context.stroke()
status = document.querySelector '#status'
drawLineGraph getClearedContext(status), [110,160,350,100,260,240], 100
render = ->
drawGraph()
drawTitle()
Listing 11.5 Drawing a line chart with HTML5 canvas
window.onload = ->
status = document.querySelector '#status'
status.style.width = '640px'
status.style.height = '480px'
canvas = document.createElement 'canvas'
canvas.width = '640'
canvas.height = '480'
status.appendChild canvas
context = canvas.getContext '2d'
drawTitle = (title) ->
context.font = 'italic 20px sans-serif'
context.fillText title
drawGraph = (buffer) ->
canvas.width = canvas.width
context.fillStyle = 'black'
context.clearRect 0, 0, 640, 480
context.fillRect 0, 0, 640, 480
context.lineWidth = 2
context.strokeStyle = '#5AB946'
context.beginPath()
prev = 0
for y, x in buffer
unless y is prev
context.lineTo 0 + x*10, 100 + y
prev = y
context.stroke()
render = (buffer) ->
drawGraph buffer
drawTitle 'Server Dashboard'
nextCallbackId = do ->
callbackId = 0
-> callbackId = callbackId + 1
nextCallbackName = ->
"callback#{nextCallbackId()}"
fetch = (src, callback) ->
head = document.querySelector 'head'
script = document.createElement 'script'
ajaxCallbackName = nextCallbackName()
window[ajaxCallbackName] = (data) ->
callback data
script.src = src + "?callback=#{ajaxCallbackName}"
head.appendChild script
seconds = (n) ->
1000*n
framesPerSecond = (n) ->
(seconds 1)/n
makeUpdater = (buffer = []) ->
bufferRenderer = (json) ->
buffer.push (JSON.parse json).hits
if buffer.length is 22 then buffer.shift()
render buffer
->
window.setInterval ->
fetch '/feed.json', bufferRenderer
, framesPerSecond 1
updater = makeUpdater()
updater()
11.5 Creating animations
seconds = (n) -> n*1000
setInterval bar.height, seconds 1
bar.style.height = 20
bar = document.querySelector '#bar'
targetHeight = 65
interval = setInterval ->
currentHeight = bar.style.height.replace /px/, ''
if currentHeight >= targetHeight
clearInterval interval
else
bar.style.height currentHeight + 1 + 'px'
, 100
animateStyleInPixels = (element, propertyName, targetValue) ->
interval = setInterval ->
currentValue = element.style[propertyName].replace /px/, ''
if currentValue >= targetValue
clearInterval interval
else
bar.style[propertyName] = currentValue + 1 + 'px'
, 100
bar = document.querySelector '.bar'
animateStyleInPixels bar, 'height', 65
Listing 11.6 Animating an immediate-mode line graph
window.onload = ->
graph = document.querySelector '#status'
graph.width = window.innerWidth
graph.height = window.innerHeight
context = graph.getContext '2d'
render = (buffer) ->
context.fillStyle = 'black'
context.clearRect 0, 0, graph.width, graph.height
context.fillRect 0, 0, graph.width, graph.height
context.lineWidth = 5
context.strokeStyle = '#5AB946'
context.beginPath()
prev = 0
for y, x in buffer()
unless y is prev
context.lineTo 0 + x, 100 + y
prev = y
context.stroke()
seconds = (n) ->
1000*n
framesPerSecond = (n) ->
(seconds 1)/n
buffer = []
nextCallbackId = do ->
callbackId = 0
-> callbackId = callbackId + 1
nextCallbackName = ->
"callback#{nextCallbackId()}"
fetch = (src, callback) ->
head = document.querySelector 'head'
script = document.createElement 'script'
ajaxCallbackName = nextCallbackName()
window[ajaxCallbackName] = (data) ->
callback data
script.src = src + "?callback=#{ajaxCallbackName}"
head.appendChild script
window.setInterval ->
fetch '/feed.json', (json) ->
render ->
buffer.push (JSON.parse json).hits
if buffer.length is graph.width then buffer.shift()
buffer
, framesPerSecond 30
11.6 Structuring programs
scene = C)zanne
.createScene('#scene')
.size(400, 400)
circle = scene
.createCircle()
.radius(10)
.color(C)zanne.RawUmber)
.position(20, 20)
circle.animatePosition 360, 360, 2
Listing 11.7 A retained-mode circle-drawing API for canvas
called Cézanne
C)zanne = do ->
seconds = (n) -> n*1000
framesPerSecond = 30
tickInterval = seconds(1)/framesPerSecond
circlePrototype =
radius: (radius) ->
@radius = radius
this
color: (hex) ->
@hex = hex
this
position: (x, y) ->
@x = x
@y = y
@context.beginPath()
@context.fillStyle = @color
@context.arc @x, @y, @radius, (Math.PI/180)*360, 0, true
@context.closePath()
@context.fill()
this
animatePosition: (x, y, duration) ->
@frames ?= []
frameCount = Math.ceil seconds(duration)/tickInterval
for n in [1..frameCount]
if n is frameCount
do =>
frame = n
@frames.unshift =>
@position x, y
else
do =>
frame = n
@frames.unshift =>
@position x/frameCount*frame, y/frameCount*frame
scenePrototype =
clear: ->
@canvas.width = @width
size: (width, height) ->
@width = width
@height = height
@canvas.width = width
@canvas.height = height
this
addElement: (element) ->
@elements ?= []
@elements.push element
element.context = @context
startClock: ->
clockTick = =>
@clear()
for element in @elements
frame = element.frames.pop()
frame?()
@clockInterval = window.setInterval clockTick, tickInterval
createCircle: ->
circle = Object.create circlePrototype
@addElement circle
circle
RawUmber: '#826644'
Viridian: '#40826d'
createScene: (selector) ->
scene = Object.create scenePrototype
node = document.querySelector selector
scene.canvas = document.createElement 'canvas'
scene.context = scene.canvas.getContext '2d'
node.appendChild scene.canvas
scene.startClock()
scene
synchronisedInterval = (fn, t, maxDrift) ->
drift = null
previous = null
compensate = 5
reset = (hard = false) ->
drift = 0 if hard
previous = Date.now()
if drift > maxDrift
setTimeout runner, (t - compensate)
else
setTimeout runner, t
runner = ->
current = Date.now()
drift += current - previous - t
previous = current
fn()
reset()
reset true
11.7 Summary