9 juin 2014

Timbre demo from Famo.us University using Meteor.js and CoffeeScript

Introduction

As already demonstrated in my previous article Famo.us polaroid tutorial in CoffeeScript and within MeteorMeteor, CoffeeScript and Famo.us are incredibly useful tools for front end or full stack developers. This article just show the same principles with the Famo.us University Timbre tutorial. A live demo is deployed on Meteor's testing ground: http://famoustimbre.meteor.com/.

Show me the code, please

If you find yourself stuck at translating some JS concept to CoffeeScript while playing the nice tutorial from Famo.us University, I provide my code hereafter:
client/stylesheets/app.styl
@import nib

html
  font-family: Helvetica

*
  -webkit-user-drag: none

body
  -webkit-touch-callout: none
  user-select: none

client/index.jade
head
  title Famo.us Timbre
  meta(name='viewport', content='width=device-width, maximum-scale=1, user-scalable=no')
  meta(name='mobile-web-app-capable', content='yes')
  meta(name='apple-mobile-web-app-capable', content='yes')
  meta(name='apple-mobile-web-app-status-bar-style', content='black')
body
  +index

template(name='index')

client/lib/famous.coffee
window.Famous ?= {}

client/lib/timbre.coffee
window.Timbre ?= {}

client/startup/famous.coffee
# Import Famous
require 'famous/core/famous'
# Adds the famo.us dependencies
require 'famous-polyfills'
# Wait for document ready
$(document).ready ->
  # Load Famo.us libraries
  Famous.Engine           = require 'famous/core/Engine'
  Famous.Surface          = require 'famous/core/Surface'
  Famous.Transform        = require 'famous/core/Transform'
  Famous.View             = require 'famous/core/View'
  Famous.Modifier         = require 'famous/core/Modifier'
  Famous.StateModifier    = require 'famous/modifiers/StateModifier'
  Famous.HeaderFooter     = require 'famous/views/HeaderFooterLayout'
  Famous.ImageSurface     = require 'famous/surfaces/ImageSurface'
  Famous.FastClick        = require 'famous/inputs/FastClick'
  Famous.GenericSync      = require 'famous/inputs/GenericSync'
  Famous.MouseSync        = require 'famous/inputs/MouseSync'
  Famous.TouchSync        = require 'famous/inputs/TouchSync'
  Famous.GenericSync.register
    'mouse': Famous.MouseSync
    'touch': Famous.TouchSync
  Famous.Easing           = require 'famous/transitions/Easing'
  Famous.Transitionable   = require 'famous/transitions/Transitionable'
  Famous.Timer            = require 'famous/utilities/Timer'
  # Create main context
  Timbre.mainCtx = Famous.Engine.createContext()

client/models/StripData.coffee
Timbre.StripData = [
  {title: 'search', iconUrl: 'img/strip-icons/famous.png'}
  {title: 'starred', iconUrl: 'img/strip-icons/starred.png'}
  {title: 'friends', iconUrl: 'img/strip-icons/friends.png'}
  {title: 'settings', iconUrl: 'img/strip-icons/settings.png'}
]

client/index.coffee
ASPECT_RATIO = 320 / 548

Template.index.rendered = ->
  $document = $ document
  $document.ready ->
    docwidth = $document.width()
    docheight = $document.height()
    if docwidth / ASPECT_RATIO > docheight
      screenwidth = docheight * ASPECT_RATIO
      screenheight = docheight
    else
      screenwidth = docwidth
      screenheight = docwidth / ASPECT_RATIO
    appView = new Timbre.AppView()
    mainMod = new Famous.Modifier
      size: [screenwidth, screenheight]
    Timbre.mainCtx
      .add mainMod
      .add appView

client/models/AppView.coffee
$(document).ready ->
  class Timbre.AppView extends Famous.View
    DEFAULT_OPTIONS:
      openPosition: 276
      transition:
        duration: 300
        curve: Famous.Easing.inOutBack
      posThreshold: 138
      velTreshold: 0.75
    constructor: (@options) ->
      @constructor.DEFAULT_OPTIONS = @DEFAULT_OPTIONS
      super @options
      @menuToggle = false
      @pageViewPos = new Famous.Transitionable 0
      @createPageView()
      @createMenuView()
      @setListeners()
      @handleSwipe()
    createPageView: ->
      @pageView = new Timbre.PageView()
      @pageModifier = new Famous.Modifier
        transform: =>
          Famous.Transform.translate @pageViewPos.get(), 0, 0
      @add(@pageModifier).add @pageView
    createMenuView: ->
      @menuView = new Timbre.MenuView stripData: Timbre.StripData
      menuModifier = new Famous.StateModifier
        transform: Famous.Transform.behind
      @add(menuModifier).add @menuView
    setListeners: ->
      @pageView.on 'menuToggle', @toggleMenu
    toggleMenu: =>
      if @menuToggle
        @slideLeft()
      else
        @slideRight()
        @menuView.animateStrips()
      @menuToggle = !@menuToggle
    slideLeft: ->
      @pageViewPos.set 0, @options.transition, =>
        @menuToggle = false
    slideRight: ->
      @pageViewPos.set @options.openPosition, @options.transition, =>
        @menuToggle = true
    handleSwipe: ->
      sync = new Famous.GenericSync(
        ['mouse', 'touch']
      , {direction: Famous.GenericSync.DIRECTION_X}
      )
      @pageView.pipe sync
      sync.on 'update', (data) =>
        currentPosition = @pageViewPos.get()
        @pageViewPos.set Math.max 0, currentPosition + data.delta
        if currentPosition is 0 and data.velocity > 0
          @menuView.animateStrips()
      sync.on 'end', (data) =>
        velocity = data.velocity
        position = @pageViewPos.get()
        if position > @options.posThreshold
          if velocity < -@options.velTreshold
            @slideLeft()
          else
            @slideRight()
        else
          if velocity > @options.velTreshold
            @slideRight()
          else
            @slideLeft()

client/models/PageView.coffee
$(document).ready ->
  class Timbre.PageView extends Famous.View
    DEFAULT_OPTIONS:
      headerSize: 44
    constructor: (@options) ->
      @constructor.DEFAULT_OPTIONS = @DEFAULT_OPTIONS
      super @options
      @createLayout()
      @createHeader()
      @createBody()
      @setListeners()
    createLayout: ->
      @layout = new Famous.HeaderFooter
        headerSize: @options.headerSize
      layoutModifier = new Famous.StateModifier
        transform: Famous.Transform.translate 0, 0, .1
      @add(layoutModifier).add @layout
    createHeader: ->
      backgroundSurface = new Famous.Surface
        properties: backgroundColor: 'black'
      backgroundModifier = new Famous.StateModifier
        transform: Famous.Transform.behind
      @layout.header
        .add backgroundModifier
        .add backgroundSurface
      @hamburgerSurface = new Famous.ImageSurface
        size: [44, 44]
        content: 'img/hamburger.png'
      searchSurface = new Famous.ImageSurface
        size: [232, 44]
        content: 'img/search.png'
      iconSurface = new Famous.ImageSurface
        size: [44, 44]
        content: 'img/icon.png'
      hamburgerModifier = new Famous.StateModifier
        origin: [0, .5]
        align: [0, .5]
      searchModifier = new Famous.StateModifier
        origin: [.5, .5]
        align: [.5, .5]
      iconModifier = new Famous.StateModifier
        origin: [1, .5]
        align: [1, .5]
      @layout.header
        .add hamburgerModifier
        .add @hamburgerSurface
      @layout.header
        .add searchModifier
        .add searchSurface
      @layout.header
        .add iconModifier
        .add iconSurface
    createBody: ->
      @bodySurface = new Famous.ImageSurface
        size: [undefined, true]
        content: 'img/body.png'
      @layout.content.add @bodySurface
    setListeners: ->
      @hamburgerSurface.on 'click', =>
        @_eventOutput.emit 'menuToggle'
      @bodySurface.pipe @_eventOutput
    createBacking: ->
      backing = new Famous.Surface
        properties:
          backgroundColor: 'black'
          boxShadow: '0 0 20px rgba(0,0,0,0.5)'
      @add backing

client/models/MenuView.coffee
$(document).ready ->
  class Timbre.PageView extends Famous.View
    DEFAULT_OPTIONS:
      headerSize: 44
    constructor: (@options) ->
      @constructor.DEFAULT_OPTIONS = @DEFAULT_OPTIONS
      super @options
      @createLayout()
      @createHeader()
      @createBody()
      @setListeners()
    createLayout: ->
      @layout = new Famous.HeaderFooter
        headerSize: @options.headerSize
      layoutModifier = new Famous.StateModifier
        transform: Famous.Transform.translate 0, 0, .1
      @add(layoutModifier).add @layout
    createHeader: ->
      backgroundSurface = new Famous.Surface
        properties: backgroundColor: 'black'
      backgroundModifier = new Famous.StateModifier
        transform: Famous.Transform.behind
      @layout.header
        .add backgroundModifier
        .add backgroundSurface
      @hamburgerSurface = new Famous.ImageSurface
        size: [44, 44]
        content: 'img/hamburger.png'
      searchSurface = new Famous.ImageSurface
        size: [232, 44]
        content: 'img/search.png'
      iconSurface = new Famous.ImageSurface
        size: [44, 44]
        content: 'img/icon.png'
      hamburgerModifier = new Famous.StateModifier
        origin: [0, .5]
        align: [0, .5]
      searchModifier = new Famous.StateModifier
        origin: [.5, .5]
        align: [.5, .5]
      iconModifier = new Famous.StateModifier
        origin: [1, .5]
        align: [1, .5]
      @layout.header
        .add hamburgerModifier
        .add @hamburgerSurface
      @layout.header
        .add searchModifier
        .add searchSurface
      @layout.header
        .add iconModifier
        .add iconSurface
    createBody: ->
      @bodySurface = new Famous.ImageSurface
        size: [undefined, true]
        content: 'img/body.png'
      @layout.content.add @bodySurface
    setListeners: ->
      @hamburgerSurface.on 'click', =>
        @_eventOutput.emit 'menuToggle'
      @bodySurface.pipe @_eventOutput
    createBacking: ->
      backing = new Famous.Surface
        properties:
          backgroundColor: 'black'
          boxShadow: '0 0 20px rgba(0,0,0,0.5)'
      @add backing

client/models/StripView.coffee
$(document).ready ->
  class Timbre.StripView extends Famous.View
    DEFAULT_OPTIONS:
      width: 320
      height: 55
      angle: -0.2
      iconSize: 32
      iconUrl: 'img/strip-icons/famous.png'
      title: 'Famo.us'
      fontSize: 26
    constructor: (@options) ->
      @constructor.DEFAULT_OPTIONS = @DEFAULT_OPTIONS
      super @options
      @createBackground()
      @createIcon()
      @createTitle()
    createBackground: ->
      backgroundSurface = new Famous.Surface
        size: [@options.width, @options.height]
        properties:
          backgroundColor: 'black'
          boxShadow: '0 0 1px rgba(0, 0, 0, 1)'
      rotateModifier = new Famous.StateModifier
        transform: Famous.Transform.rotateZ @options.angle
      skewModifier = new Famous.StateModifier
        transform: Famous.Transform.skew 0, 0, @options.angle
      @add(rotateModifier)
        .add skewModifier
        .add backgroundSurface
    createIcon: ->
      iconSurface = new Famous.ImageSurface
        size: [@options.iconSize, @options.iconSize]
        content: @options.iconUrl
        properties:
          pointerEvents: 'none'
      iconModifier = new Famous.StateModifier
        transform: Famous.Transform.translate 24, 2, 0
      @add(iconModifier).add iconSurface
    createTitle: ->
      titleSurface = new Famous.Surface
        size: [true, true]
        content: @options.title
        properties:
          color: 'white'
          fontSize: "#{@options.fontSize}px"
          textTransform: 'uppercase'
          pointerEvents: 'none'
      titleModifier = new Famous.StateModifier
        transform: Famous.Transform.thenMove(
          Famous.Transform.rotateZ @options.angle
        , [75, -5, 0]
        )
      @add(titleModifier).add titleSurface