presenter.coffee | |
---|---|
define(['require', 'underscore', 'jquery', 'jquery-plugins'],
(require, _, $) ->
logger =
log: ->
console.log.apply(console, arguments) if config.debug | |
Config settings | config =
shouldToggleScrollDownHint: false
scrollElementIntoViewOnScroll: false
scrollElementIntoViewOnResize: true
debug: false
scrollDebounceTimeInMs: 300
panelSelector: '.area'
presentationModeQuerystring: 'presentation'
fullscreenButton: false
forceWebAudioSupportMessage: false |
If the first .area element is visible in the viewport, the 'Scroll down' .hint is shown otherwise it is hidden | toggleScrollDownHint = ->
visibleEls = $('.area:in-viewport')
if $(visibleEls).is $('.area')[0]
$('nav .hint').addClass('is-visible')
else
$('nav .hint').removeClass('is-visible')
initScrollDownHint = ->
if config.shouldToggleScrollDownHint
$(window).bind('scroll', toggleScrollDownHint) |
When the 'Scroll down' arrow is pressed on the first .area element the page is scrolled to whatever the second .area element is | $('.hint').on('click', (evt) ->
evt.preventDefault()
scrollTo $('.area')[1]
)
else
$('nav .hint').removeClass('is-visible') |
Wrap the jQuery.scrollTo plugin to pass in the same options for a consistent scroll animation on the page | scrollTo = (el) ->
$.scrollTo el, axis:'y', duration:1300, easing:'easeOutQuart' |
When there are 2 .area elements visible within the browser viewport the element with the most visible height is scrolled into view. BUG: If there is only 1 element visible, no scrolling takes place due to a bug where the scrollstop event fires too many times causing a visual 'jumping' effect when scrolling. TODO: Fix so that this scrolls when only 1 element is visible | scrollMostVisibleElementIntoView = ->
logger.log('scrollMostVisibleElementIntoView start')
mostVisible = findMostVisibleEl()
logger.log('mostVisible', mostVisible)
if mostVisible?
logger.log('scrollMostVisibleElementIntoView', mostVisible.el)
scrollTo mostVisible.el
createScrollMostVisibleElementIntoViewWithDebouce = ->
debouncedFunction = $.debounce(config.scrollDebounceTimeInMs, scrollMostVisibleElementIntoView)
return debouncedFunction
inverseScale = (progress) ->
return 1 - scale(progress)
scale = (progress) ->
if progress >= 0 && progress <= 0.4
scaled = 1 - (progress * 2)
else if progress >= 0.6 && progress <= 1
scaled = (progress * 2) - 1
else
scaled = 0
return scaled
updateNavButtons = ->
windowScrollTop = $(window).scrollTop()
el = $("#{config.panelSelector}:in-viewport")[0]
if el?
offset = getOffsetsFor(el, windowScrollTop)
progress = Math.abs(offset.viewportOffset / offset.height)
opacity = scale(progress)
else
offset = 0
$('.prev.button').css 'opacity', opacity
$('.next.button').css 'opacity', opacity
currentPanelId = findCurrentPanelId()
$('body').attr('data-section', currentPanelId)
switch currentPanelId |
when "intro" then navButtons(null, 'info') | when "info" then navButtons(null, 'demo')
when "demo" then navButtons('info', 'code')
when "code" then navButtons('demo', null)
navButtons = (prevLabel, nextLabel) ->
if prevLabel
$('.prev.button').show()
.attr('href', '#' + prevLabel)
.find('.label').text(prevLabel || '')
else
$('.prev.button').hide()
if nextLabel
$('.next.button').show()
.attr('href', '#' + nextLabel)
.find('.label').text(nextLabel || '')
else
$('.next.button').hide()
findCurrentPanelId = ->
current = findMostVisibleEl()
return current?.el?.getAttribute('id')
getOffsetsFor = (element, windowScrollTop) ->
$el = $(element) |
The height of this element | height = $el.height() |
The position of the top of this element from the canvas | offsetTop = $el.offset().top |
The position of the top of this element from the viewport | viewportOffset = offsetTop - windowScrollTop
return height:height, offsetTop:offsetTop, viewportOffset:viewportOffset |
The browser canvas is the entire document The browser viewport is the area that is currently visible to the user | findMostVisibleEl = ->
visibleEls = $("#{config.panelSelector}:in-viewport")
logger.log('visible elements', visibleEls) |
Short circuit if only 1 element is visible | return el:visibleEls[0], height:null if visibleEls.length == 1 |
The distance from the top of the canvas to the top of the viewport | windowScrollTop = $(window).scrollTop()
logger.log 'window.scrollTop', windowScrollTop |
The height of the viewport (i.e. what the user can see) | viewportHeight = $(window).height()
logger.log 'viewportHeight', viewportHeight
mostVisible = null |
For all visible elements in the viewport | visibleEls.each () ->
logger.log('-----------------')
logger.log(this)
offsets = getOffsetsFor(this, windowScrollTop)
logger.log 'viewportOffset', offsets.viewportOffset, (offsets.viewportOffset > 0) |
The top of the element is visible in the viewport | if offsets.viewportOffset > 0
visibleHeight = (windowScrollTop + viewportHeight) - offsets.offsetTop |
Else the top of the element is above the viewport | else
visibleHeight = (offsets.offsetTop + offsets.height) - windowScrollTop
logger.log 'height %o, visibleHeight %o', offsets.height, visibleHeight |
This is the first element we've found | unless mostVisible?
mostVisible = el:this, height:visibleHeight
logger.log('setting mostVisible height', mostVisible?.height) |
If a mostVisible element has already been found, work out if there's more of the current element visible | if mostVisible? && (mostVisible.height < visibleHeight)
mostVisible = el:this, height:visibleHeight
logger.log('setting mostVisible height', mostVisible?.height)
logger.log('findMostVisibleEl - mostVisible', mostVisible, $('.area')[0])
return mostVisible
initScrollIntoView = -> |
Scroll an area into view when scrolling stops | if config.scrollElementIntoViewOnScroll
logger.log('config.scrollElementIntoViewOnScroll')
$(window).bind('scrollstop', createScrollMostVisibleElementIntoViewWithDebouce)
if config.scrollElementIntoViewOnResize
logger.log('config.scrollElementIntoViewOnResize')
debouncedFunction = createScrollMostVisibleElementIntoViewWithDebouce() |
Scroll area into view when browser window is resized | $(window).bind('resize', debouncedFunction)
initNavButtonUpdates = ->
$(window).bind('scroll', updateNavButtons)
$(window).bind('resize', updateNavButtons)
updateNavButtons()
getNavHeight = ->
_.reduce(
$('.project-header, .nav, .demo-header').toArray(),
(prev, current) ->
return prev + $(current).height()
,0
)
scaleContent = ->
maxWidth = 1424
minWidth = 1024
minHeight = 464
$contentAreas = $('.frame .content')
viewportHeight = $(window).height()
viewportWidth = $(window).width() |
fixedPanelHeight = getNavHeight() | logger.log('viewport height %o width %o', viewportHeight, viewportWidth)
topHeight = 25 + 49
bottomHeight = 49
contentHeight = viewportHeight - topHeight - bottomHeight - 8 - 8
contentWidth = contentHeight * 2
logger.log('contentHeight %o contentWidth %o', contentHeight, contentWidth)
if contentWidth >= maxWidth
contentWidth = maxWidth
contentHeight = 712
if contentWidth >= viewportWidth
contentWidth = viewportWidth - 8 - 8
contentHeight = contentWidth / 2
if contentWidth <= minWidth
contentWidth = minWidth
contentHeight = minWidth / 2
else if contentHeight <= minHeight
contentHeight = minHeight
contentWidth = minHeight * 2
midHeight = (contentHeight / 2)
marginTop = 0 - midHeight + ((topHeight - bottomHeight) / 2)
marginLeft = Math.round(contentWidth / 2)
margin = "#{marginTop}px 0 0 -#{marginLeft}px"
$contentAreas.css(
height: contentHeight
width: contentWidth
margin: margin
'max-height': 'none'
'max-width' : 'none'
)
$contentAreas.find('.image-block').css(
'max-height': 'none'
'max-width' : 'none'
)
transformScale = contentWidth / maxWidth
scaleMachine(transformScale)
$('.project-header, .nav, .demo-header').find('.inner')
.width(contentWidth)
logger.log('Setting content areas to height %o, width %o (%o)', contentHeight, contentWidth, $contentAreas)
initScaleContent = ->
$(window).bind('resize', scaleContent)
scaleContent()
initPresentationMode = (modernizr, querystringParam) -> |
We're in presentation mode | $('body').addClass('presentation') |
Create a scale control | $el = $("""<div class="scale-control"
style="position:fixed; right:10px; top:10px; z-index:200;
width:170px; line-height:70px; height:70px; background-color:rgba(0,0,0,0.5);
border-radius:50px; text-align:center;">
<input type="range" min=0 max=1 step=0.1 value=1 /> <span></span>
</div>""")
$input = $el.find('input')
$label = $el.find('span')
inputScale = $input.val()
$('body').append($el) |
Scale on input change | handleChangeEvent = ->
inputScale = $input.val()
$label.text(inputScale)
scaleMachine(inputScale)
$input.on('change', handleChangeEvent) |
Ensure all links also point to presentation mode | $("a[href^='/']").each ->
hrefWithQs = $(this).attr('href').replace(/#(.*)/, "?#{config.presentationModeQuerystring}#$1")
$(this).attr('href', hrefWithQs)
scaleMachine = (scaleValue) ->
transformStyleName = Modernizr.prefixed('transform')
$machine = $('#machine') |
$machine[0].style[transformStyleName] = "scale3d(#{scaleValue},#{scaleValue},0)" if $machine.length > 0 | $machine[0].style[transformStyleName] = "scale(#{scaleValue})" if $machine.length > 0
fullscreenMethod = ->
class FullscreenManager
_isFullscreen: false
enter: ->
methodName = @_method().enter
window.document.documentElement[methodName]()
@_isFullscreen = true
exit: ->
methodName = @_method().exit
window.document[methodName]()
@_isFullscreen = false
isSupported: ->
@_method()?
isFullscreen: ->
@_isFullscreen
_method: ->
docEl = document.documentElement
methods = [
{
enter: 'requestFullscreen',
exit : 'exitFullscreen'
},
{
enter: 'mozRequestFullScreen'
exit : 'mozCancelFullScreen'
},
{
enter: 'webkitRequestFullScreen',
exit : 'webkitCancelFullScreen'
}
]
method = _.find(
methods,
(item) ->
return docEl[item.enter]?
)
return method
fullscreenManager = new FullscreenManager()
toggleFullscreen = ->
return unless fullscreenManager.isSupported()
fullscreenManager.enter() unless fullscreenManager.isFullscreen()
fullscreenManager.exit() if fullscreenManager.isFullscreen()
initFullscreenUi = ->
return unless fullscreenManager.isSupported()
isFullscreen = false
$el = $('<div class="button fullscreen"><a href=""><span>Make fullscreen</span></a></div>')
$el.on(
'click',
(evt) ->
evt.preventDefault()
toggleFullscreen()
)
$('.nav').addClass('has-fullscreen')
$('.nav nav').append($el)
isWebAudioSupported = ->
return false if config.forceWebAudioSupportMessage
return AudioContext? || webkitAudioContext?
showWebAudioNotSupportedUi = ->
tmpl = $('#unsupported-browser-template').html()
el = $(tmpl)
$container = $('#demo')
$container.append(el)
$dialog = $container.find('.dialog')
$container.on(
'click',
'.mask',
(evt) ->
$target = $(evt.target) |
Close the mask and dialog if: - .close element clicked - anything outside of the dialog box | if $target.hasClass('close') || $target.has($dialog).length > 0
initDemo()
el.detach()
evt.preventDefault()
)
initDemo = ->
machine = $('body').attr('id')
if machine != 'credits'
require([machine])
init = ->
logger.log('init')
initScrollIntoView()
initNavButtonUpdates()
initScaleContent() |
When a scrolling, check if we should toggle visibility of "scroll down" message | initScrollDownHint()
initFullscreenUi() if config.fullscreenButton || new RegExp(config.presentationModeQuerystring).test(window.location.search)
initPresentationMode(Modernizr, initPresentationMode) if new RegExp(config.presentationModeQuerystring).test window.location.search
if isWebAudioSupported()
initDemo()
else
showWebAudioNotSupportedUi() |
When an internal page link is clicked, scroll to the target instead of just jumping there | $(document).on('click', "[href^='#']", (evt) ->
href = $(this).attr('href')
el = $(href)
if el.length > 0
scrollTo el
evt.preventDefault()
) |
Use stellar when the window object scrolls | $(window).stellar()
$(document).ready(init) unless /no-scroll/.test window.location.hash
)
|