diff --git a/index.html b/index.html index 46c692f..68dec87 100755 --- a/index.html +++ b/index.html @@ -6,506 +6,9 @@ + + diff --git a/magazine.js b/magazine.js new file mode 100755 index 0000000..9d1e84b --- /dev/null +++ b/magazine.js @@ -0,0 +1,503 @@ +/********************************************************************** +**********************************************************************/ + +var DEBUG = true + +/* this is needed only for live resize... */ +var PAGES_VISIBLE = 1 +var PAGES_IN_RIBBON = 6 +/*********************************************************************/ +// XXX move to generic lib... + +// this will create a function that will add/remove a css_class to elem +// calling the optional callbacks before and/or after. +// +// elem is a jquery compatible object; default use-case: a css selector. +// +// the resulting function understands the folowing arguments: +// - 'on' : switch mode on +// - 'off' : switch mode off +// - '?' : return current state ('on'|'off') +// - no arguments : toggle the state +// +// NOTE: of only one callback is given then it will be called after the +// class change... +// a way around this is to pass an empty function as callback_b +// +function createCSSClassToggler(elem, css_class, callback_a, callback_b){ + // prepare the pre/post callbacks... + if(callback_b == null){ + var callback_pre = null + var callback_post = callback_a + } else { + var callback_pre = callback_a + var callback_post = callback_b + } + // build the acual toggler function... + var func = function(action){ + if(action == null || action == '?'){ + var getter = action == '?' ? true : false + action = 'on' + // get current state... + if( $(elem).hasClass(css_class) ){ + action = 'off' + } + if(getter){ + // as the above actions indicate intent and not state, + // we'll need to swap the values... + return action == 'on' ? 'off' : 'on' + } + } + if(callback_pre != null){ + callback_pre(action) + } + // play with the class... + if(action == 'on'){ + $(elem).addClass(css_class) + } else { + $(elem).removeClass(css_class) + } + if(callback_post != null){ + callback_post(action) + } + } + func.doc = 'With no arguments this will toggle between "on" and '+ + '"off".\n'+ + 'If either "on" or "off" are given then this will switch '+ + 'to that mode.\n'+ + 'If "?" is given, this will return either "on" or "off" '+ + 'depending on the current state.' + return func +} + + +// show a jQuary opject in viewer overlay... +// XXX need to set .scrollTop(0) when showing different UI... +// ...and not set it when the UI is the same +// XXX this must create it's own overlay... +function showInOverlay(obj){ + obj.click(function(){ return false }) + // XXX + $('.viewer').addClass('overlay-mode') + // clean things up... + $('.overlay .content').children().remove() + // put it in the overlay... + $('.overlay .content').append(obj) + // prepare the overlay... + $('.overlay') + .one('click', function(){ + $('.overlay') + .fadeOut(function(){ + $('.overlay .content') + .children() + .remove() + $('.overlay-mode').removeClass('overlay-mode') + }) + }) + .fadeIn() + return obj +} + + + +function overlayMessage(text){ + return showInOverlay($('
')) +} + + + + +// XXX might be good to use apply here... +function doWithoutTransitions(obj, func, time){ + if(time == null){ + time = 5 + } + obj.addClass('unanimated') + var res = func() + setTimeout(function(){obj.removeClass('unanimated')}, time) + return res +} + +function unanimated(obj, func, time){ + return function(){ + if(time == null){ + time = 5 + } + obj.addClass('unanimated') + var res = func.apply(func, arguments) + setTimeout(function(){obj.removeClass('unanimated')}, time) + return res + } +} + +// Return a scale value for the given element(s). +// NOTE: this will only return a single scale value... +function getElementScale(elem){ + //var transform = elem.css('transform') + var vendors = ['o', 'moz', 'ms', 'webkit'] + var transform = elem.css('transform') + var res + + // go through vendor prefixes... (hate this!) + if(!transform || transform == 'none'){ + for(var i in vendors){ + transform = elem.css('-' + vendors[i] + '-transform') + if(transform && transform != 'none'){ + break + } + } + } + // no transform is set... + if(!transform || transform == 'none'){ + return 1 + } + // get the scale value -- first argument of scale/matrix... + return parseFloat((/(scale|matrix)\(([^,]*),.*\)/).exec(transform)[2]) +} + +function setElementScale(elem, scale){ + return elem.css({ + 'transform': 'scale('+scale+')', + '-moz-transform': 'scale('+scale+')', + '-o-transform': 'scale('+scale+')', + '-ms-transform': 'scale('+scale+')', + '-webkit-transform': 'scale('+scale+')', + }) +} + + + + +/*********************************************************************/ + +togglePageDragging = createCSSClassToggler( + '.viewer', + 'dragging') + + +var FIT_PAGE_TO_VIEW = true + +togglePageView = createCSSClassToggler( + '.viewer', + 'page-view-mode', + null, + // post-change callback... + function(){ + if(togglePageView('?') == 'on'){ + PAGES_VISIBLE = 1 + if(FIT_PAGE_TO_VIEW){ + fitPagesToViewer(PAGES_VISIBLE) + } else { + fitNPages(PAGES_VISIBLE) + // to prevent drag while zooming to affect + // the resulting position set it to current + // page... + // XXX now this is done by fitNPages + setCurrentPage() + } + } else { + PAGES_VISIBLE = PAGES_IN_RIBBON + if(FIT_PAGE_TO_VIEW){ + // XXX this needs to be done before transitions... + fitPagesToContent(PAGES_VISIBLE) + } else { + fitNPages(PAGES_VISIBLE) + } + } + }) + +function getPageScale(){ + return getElementScale($('.scaler')) +} + +function fitNPages(n){ + if(n==null){ + n = 1 + } + var pages = $('.page') + var view = $('.viewer') + var W = view.width() + var H = view.height() + var w = pages.width() + var h = pages.height() + + var scale = W/(w*n) + + // fit vertically if needed... + if(h*scale > H){ + scale = H/h + } + + setElementScale($('.scaler'), scale) + + /* XXX + fitPagesTo(null, n) + */ +} + +// NOTE: this is a single big function because we need to thread data +// through to avoid sampling while animating... +// XXX try and do the fitting with pure CSS... +// XXX BUG: changing width when content is constrained only horizontally +// breaks this... +function fitPagesTo(elem, n){ + if(n==null){ + n = 1 + } + var pages = $('.page') + var view = $('.viewer') + var content = $('.content') + if(elem == null){ + elem = view + } else { + elem = $(elem) + } + + // sample data... + var vW = view.width() + var vH = view.height() + var cW = content.width() + var cH = content.height() + var W = elem.width() + var H = elem.height() + var w = pages.width() + var h = pages.height() + var rW = w + var rH = h + + // NOTE: there must be no data sampling after this point... + // this is due to the fact that we will start changing stuff next + // and if CSS transitions are at play new samples will be off... + + + // XXX fitting works ONLY in one direction, if both sides need + // to be adjusted the this breaks everything... + + // do the fitting... + if(W-cW/H-cH > 1){ + rW = W * (cH/H) + pages.width(rW) + pages.height(cH) + $('.magazine').css({ + 'margin-left': -rW/2 + }) + } + if(W-cW/H-cH < 1){ + rH = H * (cW/W) + pages.height(rH) + pages.width(cW) + $('.page').css({ + 'margin-top': -rH/2 + }) + } + + // scale horizontally... + // NOTE: this is done so as to keep the scale within the content constant... + var scale = vW/(rW*n) + // or scale vertically if needed... + // XXX broken + //if(rH*scale > vH){ + // scale = vH/rH + //} + + setElementScale($('.scaler'), scale) + // update position using the new width... + setCurrentPage(null, rW) +} + + +function fitPagesToViewer(n){ + fitPagesTo('.viewer', n) +} +function fitPagesToContent(n){ + fitPagesTo('.page .content', n) +} + + +function swipeUpdate(evt, phase, direction, distance){ + var pages = $('.page') + var cur = $('.current.page') + var n = pages.index(cur) + var scale = getPageScale() + var mag = $('.magazine') + + if( phase=='move' && (direction=='left' || direction=='right') ){ + mag.addClass('unanimated') + if (direction == 'left'){ + $('.magazine').css({left: -n*cur.width()-distance/scale}) + } else if (direction == 'right') { + $('.magazine').css({left: -n*cur.width()+distance/scale}) + } + setTimeout(function(){mag.removeClass('unanimated')}, 5) + + } else if ( phase == 'start') { + togglePageDragging('on') + + } else if ( phase == 'cancel') { + togglePageDragging('off') + setCurrentPage() + + } else if ( phase =='end' ) { + togglePageDragging('off') + // see which page is closer to the middle of the screen and set it... + // do this based on how much we dragged... + var p = Math.ceil((distance/scale)/cur.width()) + + // prev page... + if(direction == 'right') { + setCurrentPage(Math.max(n-p, 0)) + // next page... + } else if (direction == 'left'){ + setCurrentPage(Math.min(n+p, pages.length-1)) + } + } +} + + +// XXX store the current page... +function setCurrentPage(n, W){ + if(n == null){ + var cur = $('.current.page') + n = $('.page').index(cur) + } else if(typeof(n) == typeof(1)) { + var cur = $($('.page')[n]) + } else { + var cur = $(n) + n = $('.page').index(cur) + } + + $('.current.page').removeClass('current') + cur.addClass('current') + + var mag = $('.magazine') + var W = W == null ? cur.width() : W + mag.css({left: -n*W}) + + // XXX should this be here??? + saveState() + + return cur +} + +function getPageNumber(page){ + if(page == null){ + page = $('.current.page') + } + return $('.page').index(page) +} + + + +function nextPage(){ + var pages = $('.page') + var cur = $('.current.page') + return setCurrentPage(Math.min(pages.index(cur)+1, pages.length-1)) +} +function prevPage(){ + var pages = $('.page') + var cur = $('.current.page') + return setCurrentPage(Math.max(pages.index(cur)-1, 0)) +} + + + +function goToMagazineCover(){ + setCurrentPage(0) +} +function goToArticleCover(){ + setCurrentPage($('.current.page').parents('.article').children('.page').first()) +} + + + +function nextArticle(){ + var cur = $('.current.page').parents('.article') + // we are at the magazine cover cover... + if(cur.length == 0){ + return setCurrentPage( + $('.article .page:first-child').first()) + } + // just find the next one... + var articles = $('.article') + return setCurrentPage( + $(articles[Math.min(articles.index(cur)+1, articles.length-1)]) + .children('.page') + .first()) + +} +// XXX this is almost exactly the same as nextArticle... +function prevArticle(){ + var cur = $('.current.page').parents('.article') + // we are at the magazine cover cover... + if(cur.length == 0){ + return $('.current.page') + } + // just find the prev one... + var articles = $('.article') + return setCurrentPage( + $(articles[Math.max(articles.index(cur)-1, 0)]) + .children('.page') + .first()) +} + + + +/*********************************************************************/ +// XXX make these magazine-specific... +// URL state managers... +function loadURLState(){ + if(window.location.hash == ''){ + return null + } + var n = parseInt(window.location.hash.split('#')[1]) + if(n != null){ + return n + } else { + alert('textual anchors not yet supported...') + return + } +} +function saveURLState(){ + window.location.hash = getPageNumber() +} + +// local storage state managers... +function loadStorageState(){ + return parseInt($.jStorage.get('current_page'), 0) +} +function saveStorageState(){ + $.jStorage.set('current_page', getPageNumber()) +} + +// generic state managers... +function loadState(){ + var n = loadURLState() + if(n != null){ + setCurrentPage(n) + } else { + setCurrentPage(loadStorageState()) + } +} +function saveState(){ + saveURLState() + saveStorageState() +} + + +/*********************************************************************/ + +// XXX create magazine... +function createMagazine(){ +} + +// XXX create article (magazine, title, position)... +function createArticle(magazine, title){ +} + +// XXX create page (article, template, position)... +function createPage(article, template){ +} + + +/*********************************************************************/ +// vim:set ts=4 sw=4 :