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