VALUE_HTML = ''
RIBBON_HTML = '' + VALUE_HTML + ''
DIGIT_HTML = '8' + RIBBON_HTML + ''
FORMAT_MARK_HTML = ''
# The bit within the parenthesis will be repeated, so (,ddd) becomes 123,456,789....
#
# If your locale uses spaces to seperate digits, you could consider using a
# Narrow No-Break Space ( ), as it's a bit more correct.
#
# Numbers will be rounded to the number of digits after the radix seperator.
#
# When values are set using `.update` or the `.innerHTML`-type attributes,
# strings are assumed to already be in the locale's format.
#
# This is just the default, it can also be set as options.format.
DIGIT_FORMAT = '(,ddd).dd'
FORMAT_PARSER = /^\(?([^)]*)\)?(?:(.)(d+))?$/
# What is our target framerate?
FRAMERATE = 30
# How long will the animation last?
DURATION = 2000
# What is the fastest we should update values when we are
# counting up (not using the wheel animation).
COUNT_FRAMERATE = 20
# What is the minimum number of frames for each value on the wheel?
# We won't render more values than could be reasonably seen
FRAMES_PER_VALUE = 2
# If more than one digit is hitting the frame limit, they would all get
# capped at that limit and appear to be moving at the same rate. This
# factor adds a boost to subsequent digits to make them appear faster.
DIGIT_SPEEDBOOST = .5
MS_PER_FRAME = 1000 / FRAMERATE
COUNT_MS_PER_FRAME = 1000 / COUNT_FRAMERATE
TRANSITION_END_EVENTS = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd'
transitionCheckStyles = document.createElement('div').style
TRANSITION_SUPPORT = transitionCheckStyles.transition? or transitionCheckStyles.webkitTransition? or
transitionCheckStyles.mozTransition? or transitionCheckStyles.oTransition?
requestAnimationFrame = window.requestAnimationFrame or window.mozRequestAnimationFrame or
window.webkitRequestAnimationFrame or window.msRequestAnimationFrame
MutationObserver = window.MutationObserver or window.WebKitMutationObserver or window.MozMutationObserver
createFromHTML = (html) ->
el = document.createElement('div')
el.innerHTML = html
el.children[0]
removeClass = (el, name) ->
el.className = el.className.replace new RegExp("(^| )#{ name.split(' ').join('|') }( |$)", 'gi'), ' '
addClass = (el, name) ->
removeClass el, name
el.className += " #{ name }"
trigger = (el, name) ->
# Custom DOM events are not supported in IE8
if document.createEvent?
evt = document.createEvent('HTMLEvents')
evt.initEvent(name, true, true)
el.dispatchEvent(evt)
now = ->
window.performance?.now?() ? +new Date
round = (val, precision=0) ->
return Math.round(val) unless precision
val *= Math.pow(10, precision)
val += 0.5
val = Math.floor(val)
val /= Math.pow(10, precision)
truncate = (val) ->
# | 0 fails on numbers greater than 2^32
if val < 0
Math.ceil(val)
else
Math.floor(val)
fractionalPart = (val) ->
val - round(val)
_jQueryWrapped = false
do wrapJQuery = ->
return if _jQueryWrapped
if window.jQuery?
_jQueryWrapped = true
# We need to wrap jQuery's .html and .text because they don't always
# call .innerHTML/.innerText
for property in ['html', 'text']
do (property) ->
old = window.jQuery.fn[property]
window.jQuery.fn[property] = (val) ->
if not val? or not this[0]?.odometer?
return old.apply this, arguments
this[0].odometer.update val
# In case jQuery is brought in after this file
setTimeout wrapJQuery, 0
class Odometer
constructor: (@options) ->
@el = @options.el
return @el.odometer if @el.odometer?
@el.odometer = @
for k, v of Odometer.options
if not @options[k]?
@options[k] = v
@options.duration ?= DURATION
@MAX_VALUES = ((@options.duration / MS_PER_FRAME) / FRAMES_PER_VALUE) | 0
@resetFormat()
@value = @cleanValue(@options.value ? '')
@renderInside()
@render()
try
for property in ['innerHTML', 'innerText', 'textContent'] when @el[property]?
do (property) =>
Object.defineProperty @el, property,
get: =>
if property is 'innerHTML'
@inside.outerHTML
else
# It's just a single HTML element, so innerText is the
# same as outerText.
@inside.innerText ? @inside.textContent
set: (val) =>
@update val
catch e
# Safari
@watchForMutations()
@
renderInside: ->
@inside = document.createElement 'div'
@inside.className = 'odometer-inside'
@el.innerHTML = ''
@el.appendChild @inside
watchForMutations: ->
# Safari doesn't allow us to wrap .innerHTML, so we listen for it
# changing.
return unless MutationObserver?
try
@observer ?= new MutationObserver (mutations) =>
newVal = @el.innerText
@renderInside()
@render @value
@update newVal
@watchMutations = true
@startWatchingMutations()
catch e
startWatchingMutations: ->
if @watchMutations
@observer.observe @el, {childList: true}
stopWatchingMutations: ->
@observer?.disconnect()
cleanValue: (val) ->
if typeof val is 'string'
# We need to normalize the format so we can properly turn it into
# a float.
val = val.replace((@format.radix ? '.'), '')
val = val.replace /[.,]/g, ''
val = val.replace '', '.'
val = parseFloat(val, 10) or 0
round(val, @format.precision)
bindTransitionEnd: ->
return if @transitionEndBound
@transitionEndBound = true
# The event will be triggered once for each ribbon, we only
# want one render though
renderEnqueued = false
for event in TRANSITION_END_EVENTS.split(' ')
@el.addEventListener event, =>
return true if renderEnqueued
renderEnqueued = true
setTimeout =>
@render()
renderEnqueued = false
trigger @el, 'odometerdone'
, 0
true
, false
resetFormat: ->
format = @options.format ? DIGIT_FORMAT
format or= 'd'
parsed = FORMAT_PARSER.exec format
if not parsed
throw new Error "Odometer: Unparsable digit format"
[repeating, radix, fractional] = parsed[1..3]
precision = fractional?.length or 0
@format = {repeating, radix, precision}
render: (value=@value) ->
@stopWatchingMutations()
@resetFormat()
@inside.innerHTML = ''
theme = @options.theme
classes = @el.className.split(' ')
newClasses = []
for cls in classes when cls.length
if match = /^odometer-theme-(.+)$/.exec(cls)
theme = match[1]
continue
if /^odometer(-|$)/.test(cls)
continue
newClasses.push cls
newClasses.push 'odometer'
unless TRANSITION_SUPPORT
newClasses.push 'odometer-no-transitions'
if theme
newClasses.push "odometer-theme-#{ theme }"
else
# This class matches all themes, so it should do what you'd expect if only one
# theme css file is brought into the page.
newClasses.push "odometer-auto-theme"
@el.className = newClasses.join(' ')
@ribbons = {}
@digits = []
wholePart = not @format.precision or not fractionalPart(value) or false
for digit in value.toString().split('').reverse()
if digit is '.'
wholePart = true
@addDigit digit, wholePart
@startWatchingMutations()
update: (newValue) ->
newValue = @cleanValue newValue
return unless diff = newValue - @value
removeClass @el, 'odometer-animating-up odometer-animating-down odometer-animating'
if diff > 0
addClass @el, 'odometer-animating-up'
else
addClass @el, 'odometer-animating-down'
@stopWatchingMutations()
@animate newValue
@startWatchingMutations()
setTimeout =>
# Force a repaint
@el.offsetHeight
addClass @el, 'odometer-animating'
, 0
@value = newValue
renderDigit: ->
createFromHTML DIGIT_HTML
insertDigit: (digit, before) ->
if before?
@inside.insertBefore digit, before
else if not @inside.children.length
@inside.appendChild digit
else
@inside.insertBefore digit, @inside.children[0]
addSpacer: (chr, before, extraClasses) ->
spacer = createFromHTML FORMAT_MARK_HTML
spacer.innerHTML = chr
addClass(spacer, extraClasses) if extraClasses
@insertDigit spacer, before
addDigit: (value, repeating=true) ->
if value is '-'
return @addSpacer value, null, 'odometer-negation-mark'
if value is '.'
return @addSpacer (@format.radix ? '.'), null, 'odometer-radix-mark'
if repeating
resetted = false
while true
if not @format.repeating.length
if resetted
throw new Error "Bad odometer format without digits"
@resetFormat()
resetted = true
chr = @format.repeating[@format.repeating.length - 1]
@format.repeating = @format.repeating.substring(0, @format.repeating.length - 1)
break if chr is 'd'
@addSpacer chr
digit = @renderDigit()
digit.querySelector('.odometer-value').innerHTML = value
@digits.push digit
@insertDigit digit
animate: (newValue) ->
if not TRANSITION_SUPPORT or @options.animation is 'count'
@animateCount newValue
else
@animateSlide newValue
animateCount: (newValue) ->
return unless diff = +newValue - @value
start = last = now()
cur = @value
do tick = =>
if (now() - start) > @options.duration
@value = newValue
@render()
trigger @el, 'odometerdone'
return
delta = now() - last
if delta > COUNT_MS_PER_FRAME
last = now()
fraction = delta / @options.duration
dist = diff * fraction
cur += dist
@render Math.round cur
if requestAnimationFrame?
requestAnimationFrame tick
else
setTimeout tick, COUNT_MS_PER_FRAME
getDigitCount: (values...) ->
for value, i in values
values[i] = Math.abs(value)
max = Math.max values...
Math.ceil(Math.log(max + 1) / Math.log(10))
getFractionalDigitCount: (values...) ->
# This assumes the value has already been rounded to
# @format.precision places
#
parser = /^\-?\d*\.(\d*?)0*$/
for value, i in values
values[i] = value.toString()
parts = parser.exec values[i]
if not parts?
values[i] = 0
else
values[i] = parts[1].length
Math.max values...
resetDigits: ->
@digits = []
@ribbons = []
@inside.innerHTML = ''
@resetFormat()
animateSlide: (newValue) ->
oldValue = @value
fractionalCount = @getFractionalDigitCount oldValue, newValue
if fractionalCount
newValue = newValue * Math.pow(10, fractionalCount)
oldValue = oldValue * Math.pow(10, fractionalCount)
return unless diff = newValue - oldValue
@bindTransitionEnd()
digitCount = @getDigitCount(oldValue, newValue)
digits = []
boosted = 0
# We create a array to represent the series of digits which should be
# animated in each column
for i in [0...digitCount]
start = truncate(oldValue / Math.pow(10, (digitCount - i - 1)))
end = truncate(newValue / Math.pow(10, (digitCount - i - 1)))
dist = end - start
if Math.abs(dist) > @MAX_VALUES
# We need to subsample
frames = []
# Subsequent digits need to be faster than previous ones
incr = dist / (@MAX_VALUES + @MAX_VALUES * boosted * DIGIT_SPEEDBOOST)
cur = start
while (dist > 0 and cur < end) or (dist < 0 and cur > end)
frames.push Math.round cur
cur += incr
if frames[frames.length - 1] isnt end
frames.push end
boosted++
else
frames = [start..end]
# We only care about the last digit
for frame, i in frames
frames[i] = Math.abs(frame % 10)
digits.push frames
@resetDigits()
for frames, i in digits.reverse()
if not @digits[i]
@addDigit ' ', (i >= fractionalCount)
@ribbons[i] ?= @digits[i].querySelector('.odometer-ribbon-inner')
@ribbons[i].innerHTML = ''
if diff < 0
frames = frames.reverse()
for frame, j in frames
numEl = document.createElement('div')
numEl.className = 'odometer-value'
numEl.innerHTML = frame
@ribbons[i].appendChild numEl
if j == frames.length - 1
addClass numEl, 'odometer-last-value'
if j == 0
addClass numEl, 'odometer-first-value'
if start < 0
@addDigit '-'
mark = @inside.querySelector('.odometer-radix-mark')
mark.parent.removeChild(mark) if mark?
if fractionalCount
@addSpacer @format.radix, @digits[fractionalCount - 1], 'odometer-radix-mark'
Odometer.options = window.odometerOptions ? {}
setTimeout ->
# We do this in a seperate pass to allow people to set
# window.odometerOptions after bringing the file in.
if window.odometerOptions
for k, v of window.odometerOptions
Odometer.options[k] ?= v
, 0
Odometer.init = ->
if not document.querySelectorAll?
# IE 7 or 8 in Quirksmode
return
elements = document.querySelectorAll (Odometer.options.selector or '.odometer')
for el in elements
el.odometer = new Odometer {el, value: (el.innerText ? el.textContent)}
if document.documentElement?.doScroll? and document.createEventObject?
# IE < 9
_old = document.onreadystatechange
document.onreadystatechange = ->
if document.readyState is 'complete' and Odometer.options.auto isnt false
Odometer.init()
_old?.apply this, arguments
else
document.addEventListener 'DOMContentLoaded', ->
if Odometer.options.auto isnt false
Odometer.init()
, false
if typeof define is 'function' and define.amd
# AMD. Register as an anonymous module.
define ['jquery'], ->
Odometer
else if typeof exports is not 'undefined'
# CommonJS
module.exports = Odometer
else
# Browser globals
window.Odometer = Odometer