├── Procfile ├── src └── app │ ├── lib │ ├── config.coffee │ ├── months.coffee │ ├── ratio.coffee │ ├── tools.coffee │ ├── get.coffee │ ├── queue.coffee │ ├── imageLoader.coffee │ ├── router.coffee │ └── scroll.coffee │ ├── views │ ├── loadingView.styl │ ├── appView.styl │ ├── body.styl │ ├── forkView.styl │ ├── loadingView.coffee │ ├── forkView.coffee │ ├── imageView.styl │ ├── photosGroupsView.styl │ ├── fullscreenView.styl │ ├── mainView.styl │ ├── photosGroupView.styl │ ├── svgView.coffee │ ├── timelineView.styl │ ├── photosGroupView.coffee │ ├── imageView.coffee │ ├── photosGroupsView.coffee │ └── timelineView.coffee │ ├── app.styl │ ├── app.nomodule.coffee │ ├── base │ ├── controller.coffee │ ├── module.coffee │ ├── eventDispatcher.coffee │ └── view.coffee │ ├── mixins.styl │ ├── controllers │ ├── app.coffee │ ├── timeline.coffee │ └── fullscreen.coffee │ └── vendor │ └── dynamics.js ├── .gitignore ├── views ├── layouts │ └── main.hbs └── index.hbs ├── scripts ├── compile.coffee ├── data │ └── commonjs.nomodule.js └── prepare.coffee ├── package.json ├── web.coffee ├── lib └── compile.coffee └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: coffee web.coffee 2 | -------------------------------------------------------------------------------- /src/app/lib/config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | public/data/*/* 3 | public/data/* 4 | public/*.js 5 | public/*.css 6 | node_modules 7 | -------------------------------------------------------------------------------- /src/app/views/loadingView.styl: -------------------------------------------------------------------------------- 1 | .loadingView { 2 | background: #0091FF 3 | height: 2px 4 | width: 0% 5 | } 6 | -------------------------------------------------------------------------------- /src/app/lib/months.coffee: -------------------------------------------------------------------------------- 1 | module.exports = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 2 | -------------------------------------------------------------------------------- /src/app/lib/ratio.coffee: -------------------------------------------------------------------------------- 1 | pixelRatio = window.devicePixelRatio ? 1 2 | pixelRatio = {1:1, 2:2}[pixelRatio] ? 1 3 | module.exports = "#{pixelRatio}x" 4 | -------------------------------------------------------------------------------- /src/app/app.styl: -------------------------------------------------------------------------------- 1 | //= require_tree ./base 2 | //= require_tree ./lib 3 | //= require_tree ./controllers 4 | //= require_tree ./views 5 | //= require_self 6 | -------------------------------------------------------------------------------- /src/app/app.nomodule.coffee: -------------------------------------------------------------------------------- 1 | #= require_tree ./base 2 | #= require_tree ./lib 3 | #= require_tree ./controllers 4 | #= require_tree ./views 5 | #= require_tree ./vendor 6 | #= require_self 7 | -------------------------------------------------------------------------------- /src/app/views/appView.styl: -------------------------------------------------------------------------------- 1 | .appView { 2 | > .loadingViewContainer { 3 | position: fixed 4 | z-index: 15 5 | top: 50% 6 | left: 30% 7 | right: 30% 8 | height: 2px 9 | margin-top: -1px 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/base/controller.coffee: -------------------------------------------------------------------------------- 1 | EventDispatcher = require('eventDispatcher') 2 | 3 | class Controller extends EventDispatcher 4 | constructor: (options = {}) -> 5 | super 6 | @options = options 7 | @view = null 8 | 9 | module.exports = Controller 10 | -------------------------------------------------------------------------------- /src/app/views/body.styl: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0 3 | padding: 0 4 | font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif 5 | user-select: none 6 | -webkit-user-select: none 7 | -webkit-font-smoothing: antialiased 8 | } 9 | 10 | html { 11 | height: 100% 12 | } 13 | -------------------------------------------------------------------------------- /src/app/views/forkView.styl: -------------------------------------------------------------------------------- 1 | .forkView { 2 | position: fixed 3 | bottom: 15px 4 | right: 16px 5 | color: #A0A0A0 6 | font-size: 12px 7 | text-decoration: none 8 | } 9 | 10 | @media all and (max-width: 750px) { 11 | .forkView { 12 | display: none 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/views/loadingView.coffee: -------------------------------------------------------------------------------- 1 | View = require('view') 2 | tools = require('tools') 3 | 4 | class LoadingView extends View 5 | className: 'loadingView' 6 | 7 | setValue: (value) => 8 | @el.style.width = "#{tools.roundf(value * 100, 2)}%" 9 | 10 | module.exports = LoadingView 11 | -------------------------------------------------------------------------------- /src/app/lib/tools.coffee: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | 3 | module.exports.roundf = (v, decimal) -> 4 | d = Math.pow(10, decimal) 5 | return Math.round(v * d) / d 6 | 7 | module.exports.merge = (a, b) -> 8 | c = {} 9 | for k, v of a 10 | c[k] = v 11 | for k, v of b 12 | c[k] = v 13 | c 14 | -------------------------------------------------------------------------------- /src/app/lib/get.coffee: -------------------------------------------------------------------------------- 1 | get = (path, callback) -> 2 | httpRequest = new XMLHttpRequest() 3 | httpRequest.onreadystatechange = -> 4 | if httpRequest.readyState == 4 and httpRequest.status == 200 5 | callback?(JSON.parse(httpRequest.responseText)) 6 | httpRequest.open('GET', path) 7 | httpRequest.send() 8 | 9 | module.exports = get 10 | -------------------------------------------------------------------------------- /src/app/views/forkView.coffee: -------------------------------------------------------------------------------- 1 | View = require('view') 2 | 3 | class ForkView extends View 4 | tag: 'a' 5 | className: 'forkView' 6 | 7 | constructor: -> 8 | super 9 | 10 | @text("Fork me on Github") 11 | @el.href = "https://github.com/michaelvillar/photoslog" 12 | @el.target = "_blank" 13 | 14 | module.exports = ForkView 15 | -------------------------------------------------------------------------------- /src/app/views/imageView.styl: -------------------------------------------------------------------------------- 1 | .imageView { 2 | background-repeat: no-repeat 3 | background-size: contain 4 | background-position: center center 5 | position: relative 6 | overflow: hidden 7 | 8 | > .cover { 9 | position: absolute 10 | top: 0 11 | left: 0 12 | width: 100% 13 | height: 100% 14 | background: #4b4b4b 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/views/photosGroupsView.styl: -------------------------------------------------------------------------------- 1 | .photosGroupsView { 2 | margin-top: 45px 3 | will-change: transform 4 | 5 | > .photosGroupView { 6 | margin-bottom: 53px 7 | } 8 | } 9 | 10 | @media all and (max-width: 750px) { 11 | .photosGroupsView { 12 | margin-top: 30px 13 | 14 | > .photosGroupView { 15 | margin-bottom: 35px 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | {{#if representativeImagePath}} 8 | 9 | {{/if}} 10 | 11 | 12 | {{{body}}} 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/base/module.coffee: -------------------------------------------------------------------------------- 1 | moduleKeywords = ['extended', 'included'] 2 | 3 | class Module 4 | @extend: (obj) -> 5 | for name, func of obj when name not in moduleKeywords 6 | @[name] = func 7 | 8 | obj.extended?.apply(@) 9 | this 10 | 11 | @include: (obj) -> 12 | for name, func of obj when name not in moduleKeywords 13 | # Assign properties to the prototype 14 | @::[name] = func 15 | 16 | obj.included?.apply(@) 17 | this 18 | 19 | module.exports = Module 20 | -------------------------------------------------------------------------------- /scripts/compile.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ./node_modules/coffee-script/bin/coffee 2 | compile = require('../lib/compile') 3 | watchTree = require("fs-watch-tree").watchTree 4 | argv = require('argv') 5 | args = argv.option([{ 6 | name: 'watch', 7 | short: 'w', 8 | type: 'boolean' 9 | }]).run() 10 | 11 | build = -> 12 | compile (e) -> 13 | console.log(e) if e? 14 | 15 | if args.options.watch 16 | watchTree "src/app/", (e) -> 17 | console.log("\nFile changed: " + e.name) 18 | build() 19 | 20 | build() 21 | -------------------------------------------------------------------------------- /src/app/mixins.styl: -------------------------------------------------------------------------------- 1 | vendor(prop, args) 2 | -webkit-{prop}: args 3 | -moz-{prop}: args 4 | -ms-{prop}: args 5 | -o-{prop}: args 6 | {prop}: args 7 | 8 | transition() 9 | vendor('transition', arguments) 10 | 11 | transition-delay() 12 | vendor('transition-delay', arguments) 13 | 14 | transform-origin() 15 | vendor('transform-origin', arguments) 16 | 17 | transform(tr) 18 | vendor('transform', arguments) 19 | 20 | user-select() 21 | vendor('user-select', arguments) 22 | 23 | box-sizing() 24 | vendor('box-sizing', arguments) 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PhotoLog", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "argv": "0.x.x", 6 | "coffee-script": "^1.12.7", 7 | "connect": "^2.30.2", 8 | "easyimage": "1.x.x", 9 | "express": "^4.16.3", 10 | "express-handlebars": "^2.0.1", 11 | "fs-watch-tree": "0.x.x", 12 | "lodash": "2.x.x", 13 | "mincer": "1.x.x", 14 | "mkdirp": "x.x.x", 15 | "q": "1.x.x", 16 | "q-io": "1.x.x", 17 | "request": "^2.85.0", 18 | "stylus": "0.x.x" 19 | }, 20 | "scripts": { 21 | "postinstall": "./scripts/compile.coffee" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/views/fullscreenView.styl: -------------------------------------------------------------------------------- 1 | .fullscreenView { 2 | position: fixed 3 | top: 0 4 | left: 0 5 | right: 0 6 | bottom: 0 7 | z-index: 10 8 | cursor: pointer 9 | -webkit-tap-highlight-color: rgba(0,0,0,0) 10 | 11 | > .backgroundView { 12 | position: absolute 13 | top: 0 14 | left: 0 15 | right: 0 16 | bottom: 0 17 | background: rgb(20, 20, 20) 18 | transform: translateZ(0) 19 | } 20 | 21 | > .imageView { 22 | position: absolute 23 | display: block 24 | } 25 | } 26 | 27 | .imageView.mainImageView { 28 | position: absolute 29 | display: block 30 | z-index: 11 31 | } 32 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 | 15 |
Loading
16 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /src/app/lib/queue.coffee: -------------------------------------------------------------------------------- 1 | EventDispatcher = require('eventDispatcher') 2 | 3 | class Queue extends EventDispatcher 4 | constructor: -> 5 | @jobs = [] 6 | @runningJobs = [] 7 | @maxConcurrent = 2 8 | 9 | cancelAllJobs: => 10 | for entry in @jobs 11 | entry.options.cancelled?() 12 | @jobs = [] 13 | 14 | addJob: (job, options = {}) => 15 | @jobs.push({ job: job, options: options }) 16 | @tick() 17 | 18 | tick: => 19 | return if @maxConcurrent <= @runningJobs.length 20 | return if @jobs.length <= 0 21 | 22 | entry = @jobs[0] 23 | job = entry.job 24 | @jobs.splice(0, 1) 25 | @runningJobs.push(job) 26 | job => 27 | entry.options.complete?() 28 | pos = @runningJobs.indexOf(job) 29 | @runningJobs.splice(pos, 1) 30 | @tick() 31 | 32 | module.exports = Queue 33 | -------------------------------------------------------------------------------- /src/app/views/mainView.styl: -------------------------------------------------------------------------------- 1 | .mainView { 2 | .timelineView { 3 | position: fixed 4 | top: 0 5 | left: 0 6 | bottom: 0 7 | } 8 | 9 | .photosGroupsContainerView { 10 | position: relative 11 | left: 290px 12 | width: calc(100% - 280px) 13 | max-width: 60% 14 | 15 | .photosGroupsView { 16 | max-width: 750px 17 | } 18 | } 19 | } 20 | 21 | @media all and (min-width: 1250px) { 22 | .mainView { 23 | .timelineView { 24 | left: calc((100% - 1250px) / 2) 25 | } 26 | 27 | .photosGroupsContainerView { 28 | left: calc((100% - 1250px) / 2 + 290px) 29 | width: 750px 30 | } 31 | } 32 | } 33 | 34 | @media all and (max-width: 750px) { 35 | .mainView { 36 | .timelineView { 37 | display: none 38 | } 39 | 40 | .photosGroupsContainerView { 41 | position: static 42 | left: 0 43 | margin-left: 7px 44 | width: calc(100% - 14px) 45 | max-width: 100% 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/lib/imageLoader.coffee: -------------------------------------------------------------------------------- 1 | EventDispatcher = require('eventDispatcher') 2 | 3 | loads = {} 4 | 5 | class Load extends EventDispatcher 6 | constructor: -> 7 | super 8 | @progress = null 9 | 10 | setProgress: (@progress) => 11 | @trigger('progress', @) 12 | 13 | setURL: (@url) => 14 | @trigger('load', @) 15 | 16 | module.exports = {} 17 | module.exports.get = (url) -> 18 | load = loads[url] 19 | return load if load? 20 | loads[url] = load = new Load 21 | 22 | xhr = new XMLHttpRequest() 23 | xhr.onload = (e) => 24 | h = xhr.getAllResponseHeaders() 25 | m = h.match(/^Content-Type\:\s*(.*?)$/mi) 26 | mimeType = m[1] || 'image/png'; 27 | 28 | blob = new Blob([xhr.response], { type: mimeType }) 29 | load.setURL(window.URL.createObjectURL(blob)) 30 | 31 | xhr.onprogress = (e) => 32 | if e.lengthComputable 33 | load.setProgress(parseInt((e.loaded / e.total) * 100)) 34 | 35 | xhr.onloadstart = => 36 | load.setProgress(0) 37 | 38 | xhr.onloadend = => 39 | load.setProgress(100) 40 | 41 | xhr.open('GET', url, true) 42 | xhr.responseType = 'arraybuffer' 43 | xhr.send() 44 | 45 | return load 46 | -------------------------------------------------------------------------------- /src/app/base/eventDispatcher.coffee: -------------------------------------------------------------------------------- 1 | Module = require('module') 2 | 3 | class EventDispatcher extends Module 4 | constructor: -> 5 | @eventCallbacks = {} 6 | 7 | on: (eventName, callback) => 8 | @eventCallbacks[eventName] ||= [] 9 | @eventCallbacks[eventName].push callback 10 | 11 | off: (eventName = null, callback = null) => 12 | if !eventName 13 | @eventCallbacks = {} 14 | else if !callback 15 | @eventCallbacks[eventName] = [] 16 | else 17 | @eventCallbacks[eventName] = @eventCallbacks[eventName].map (cb) -> 18 | cb if cb != callback 19 | 20 | trigger: (eventName, args...) => 21 | callbacks = @eventCallbacks[eventName] 22 | return unless callbacks? 23 | callbacks = callbacks.slice() 24 | for callback in callbacks 25 | callback(args...) 26 | 27 | triggerToSubviews: (eventName, args...) => 28 | @trigger.apply(@, arguments) 29 | if @subviews? 30 | for subview in @subviews 31 | subview.triggerToSubviews.apply(subview, arguments) 32 | 33 | propagateEvent: (event, source) -> 34 | source.on(event, (args...) => 35 | @trigger(event, args...) 36 | ) 37 | 38 | module.exports = EventDispatcher 39 | -------------------------------------------------------------------------------- /web.coffee: -------------------------------------------------------------------------------- 1 | request = require('request') 2 | express = require('express') 3 | 4 | port = process.env.PORT || 8000 5 | representativeImage = null 6 | 7 | app = express() 8 | 9 | app.use(express.static(__dirname + '/public')) 10 | app.get('*', (req, res) -> 11 | root = process.env.IMAGES_ROOT_PATH ? "/data/" 12 | if representativeImage? 13 | representativeImagePath = root + representativeImage.files["1x"] 14 | res.render('index', { 15 | title: process.env.PAGE_TITLE ? "Photos Log", 16 | imagesRootPath: root, 17 | representativeImagePath: representativeImagePath 18 | }) 19 | ) 20 | 21 | # Handlebars 22 | expressHbs = require('express-handlebars') 23 | app.engine('hbs', expressHbs({extname:'hbs', defaultLayout:'main.hbs'})) 24 | app.set('view engine', 'hbs') 25 | 26 | app.listen(port) 27 | console.info("Listening on port " + port) 28 | 29 | # Load info.json 30 | request.get (process.env.IMAGES_ROOT_PATH ? "http://localhost:#{port}/data/") + "info.json", (error, response, body) -> 31 | if !error && response.statusCode == 200 32 | data = JSON.parse(body) 33 | groups = data.groups 34 | lastGroup = groups[groups.length - 1] 35 | representativeImage = lastGroup.images[0] 36 | -------------------------------------------------------------------------------- /src/app/lib/router.coffee: -------------------------------------------------------------------------------- 1 | EventDispatcher = require('eventDispatcher') 2 | scroll = require('scroll') 3 | 4 | router = new EventDispatcher 5 | 6 | router.goToHome = (options = {}) -> 7 | router.goTo({}, "", options) 8 | 9 | router.goToGroup = (group, options = {}) -> 10 | if !group? 11 | return router.goToHome() 12 | state = { 13 | obj: group.path, 14 | type: 'group' 15 | } 16 | router.goTo(state, "/#{group.path}", options) 17 | 18 | router.goTo = (state, url, options = {}) -> 19 | options.trigger ?= true 20 | return if JSON.stringify(router.state) == JSON.stringify(state) 21 | # history.pushState(state, "", url) 22 | router.state = state 23 | router.trigger('change', state) if options.trigger 24 | 25 | router.state = {} 26 | 27 | parse = -> 28 | path = window.location.pathname 29 | match = path.match(/\/([^\/]*)\/?/) 30 | if match? and match[1]? and match[1].length > 0 31 | router.state = { 32 | obj: match[1] + '/', 33 | type: 'group' 34 | } 35 | 36 | init = -> 37 | window.addEventListener 'popstate', (e) => 38 | return unless e.state 39 | router.state = e.state 40 | router.trigger('change', e.state) 41 | 42 | parse() 43 | 44 | init() 45 | 46 | module.exports = router 47 | -------------------------------------------------------------------------------- /src/app/views/photosGroupView.styl: -------------------------------------------------------------------------------- 1 | @import "../mixins.styl" 2 | 3 | .photosGroupView { 4 | width: 100% 5 | line-height: 0 6 | 7 | > h2 { 8 | display: none 9 | } 10 | 11 | > .imageView { 12 | display: inline-block 13 | cursor: pointer 14 | -webkit-tap-highlight-color: rgba(0,0,0,0) 15 | background-color: #fff 16 | background-size: cover 17 | 18 | &.loaded { 19 | transform: translateZ(0) 20 | } 21 | 22 | &.full { 23 | width: 100% 24 | margin-bottom: 7px 25 | } 26 | 27 | &.row { 28 | margin-bottom: 7px 29 | &:not(:last-child) { 30 | margin-right: 7px 31 | } 32 | } 33 | } 34 | 35 | &.disabled { 36 | > .imageView { 37 | background-color: #B0B0B0 38 | } 39 | } 40 | } 41 | 42 | @media all and (max-width: 750px) { 43 | .photosGroupView { 44 | > h2 { 45 | display: block 46 | font-family: "HelveticaNeueLight", "HelveticaNeue-Light", "Helvetica-Neue-Light", "Helvetica", Arial, sans-serif 47 | font-size: 18px 48 | font-weight: normal 49 | cursor: default 50 | color: #565656 51 | position: relative 52 | top: -12px 53 | 54 | > span { 55 | float: right 56 | font-family: "HelveticaNeueLight", "HelveticaNeue-Light", "Helvetica-Neue-Light", "Helvetica", Arial, sans-serif 57 | font-size: 11px 58 | color: #565656 59 | padding-top: 3px 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/compile.coffee: -------------------------------------------------------------------------------- 1 | fs = require('fs') 2 | Mincer = require('mincer') 3 | mkdirp = require('mkdirp') 4 | 5 | environment = new Mincer.Environment() 6 | environment.appendPath('src/app') 7 | 8 | class JSPostProcessor 9 | constructor: (@path, @data) -> 10 | 11 | evaluate: (context, locals) => 12 | if(this.path.match(/nomodule/)) 13 | return this.data 14 | pathArgs = this.path.split("/") 15 | name = pathArgs[pathArgs.length - 1].replace(".coffee","").replace(".js","") 16 | data = 'this.require.define({ "'+name+'" : function(exports, require, module) {' 17 | data += this.data 18 | data += '}});' 19 | return data 20 | 21 | environment.registerPostProcessor('application/javascript', JSPostProcessor) 22 | 23 | commonjsData = fs.readFileSync('scripts/data/commonjs.nomodule.js') 24 | 25 | saveFile = (path, filename, content, callback) -> 26 | mkdirp(path, (e) -> 27 | if e? 28 | return callback(e) 29 | fs.writeFile path + "/" + filename, content, callback 30 | ) 31 | 32 | module.exports = (callback)-> 33 | try 34 | asset = environment.findAsset('app.nomodule.coffee') 35 | catch e 36 | return callback(e) 37 | saveFile "public/js", "app.js", commonjsData + asset.toString(), (e) -> 38 | if(e?) 39 | return callback(e) 40 | console.log 'Compiled app.js' 41 | 42 | try 43 | asset = environment.findAsset('app.styl') 44 | catch e 45 | return callback(e) 46 | saveFile "public/css", "app.css", asset.toString(), (e) -> 47 | if(e?) 48 | return callback(e) 49 | console.log 'Compiled app.css' 50 | callback(null) 51 | -------------------------------------------------------------------------------- /scripts/data/commonjs.nomodule.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (!this.require) { 3 | var modules = {}, cache = {}; 4 | 5 | var require = function(name, root) { 6 | var path = expand(root, name), indexPath = expand(path, './index'), module, fn; 7 | module = cache[path] || cache[indexPath]; 8 | if (module) { 9 | return module; 10 | } else if (fn = modules[path] || modules[path = indexPath]) { 11 | module = {id: path, exports: {}}; 12 | cache[path] = module.exports; 13 | fn(module.exports, function(name) { 14 | return require(name, dirname(path)); 15 | }, module); 16 | return cache[path] = module.exports; 17 | } else { 18 | throw 'module ' + name + ' not found'; 19 | } 20 | }; 21 | 22 | var expand = function(root, name) { 23 | var results = [], parts, part; 24 | // If path is relative 25 | if (/^\.\.?(\/|$)/.test(name)) { 26 | parts = [root, name].join('/').split('/'); 27 | } else { 28 | parts = name.split('/'); 29 | } 30 | for (var i = 0, length = parts.length; i < length; i++) { 31 | part = parts[i]; 32 | if (part == '..') { 33 | results.pop(); 34 | } else if (part != '.' && part != '') { 35 | results.push(part); 36 | } 37 | } 38 | return results.join('/'); 39 | }; 40 | 41 | var dirname = function(path) { 42 | return path.split('/').slice(0, -1).join('/'); 43 | }; 44 | 45 | this.require = function(name) { 46 | return require(name, ''); 47 | }; 48 | 49 | this.require.define = function(bundle) { 50 | for (var key in bundle) { 51 | modules[key] = bundle[key]; 52 | } 53 | }; 54 | 55 | this.require.modules = modules; 56 | this.require.cache = cache; 57 | } 58 | 59 | return this.require; 60 | }).call(this); 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | [http://travel.michaelvillar.com](http://travel.michaelvillar.com) 3 | 4 | # Usage 5 | 6 | ## Photos 7 | Create a `data` folder: this is where your photos will live. 8 | 9 | For each group of photos, create a folder (i.e. "sanfrancisco") and add your photos in it. 10 | 11 | Finally, create a `info.json` into that folder with this format: 12 | ``` 13 | { 14 | "name": "San Francisco", 15 | "date": "2015-01-01", 16 | "images": [ 17 | { 18 | "file": "1.jpg", 19 | "type": "full" 20 | }, 21 | { 22 | "file": "2.jpg", 23 | "type": "row" 24 | }, 25 | { 26 | "file": "3.jpg", 27 | "type": "row" 28 | } 29 | ] 30 | } 31 | ``` 32 | This is how the file hierarchy should look like: 33 | ![File hierarchy](http://michaelvillar.s3.amazonaws.com/images/photos_log_file_hierarchy.png) 34 | 35 | ## Test it locally 36 | Install the necessary packages: 37 | ``` 38 | brew install imagemagick 39 | npm install 40 | ``` 41 | Then run the script to prepare your photos -- this can take a while. 42 | It will resize your photos and create a global `info.json` required for the app. 43 | These will be saved into `public/data/`. 44 | ``` 45 | ./scripts/prepare.coffee 46 | ``` 47 | Finally, compile the app and run it! 48 | ``` 49 | ./scripts/compile.coffee 50 | coffee web.coffee 51 | ``` 52 | Open [http://localhost:8000/](http://localhost:8000/) in your browser. 53 | 54 | ## Deploy it 55 | For the sake of simplicity, we'll use Heroku. 56 | ``` 57 | heroku apps:create 58 | git push heroku master 59 | ``` 60 | Then, you need to upload your photos on a server. Amazon S3 is an option. 61 | Upload the whole `public/data` folder which has been generated by `./scripts/prepare.coffee`. 62 | Tell the app where your photos live: 63 | ``` 64 | heroku config:set IMAGES_ROOT_PATH="http://example.com/folder/" 65 | ``` 66 | You can also change the page title: 67 | ``` 68 | heroku config:set PAGE_TITLE="Michael Villar - Travel Log" 69 | ``` 70 | -------------------------------------------------------------------------------- /src/app/views/svgView.coffee: -------------------------------------------------------------------------------- 1 | View = require('view') 2 | 3 | class SVGView extends View 4 | className: 'svg' 5 | tag: 'svg' 6 | xmlns: "http://www.w3.org/2000/svg" 7 | 8 | constructor: -> 9 | super 10 | 11 | if @tag == 'svg' 12 | @el.setAttributeNS(null, 'version', "1.1") 13 | # @el.setAttributeNS(null, 'viewBox', "0 0 100% 100%") 14 | @el.setAttributeNS(null, 'width', "100%") 15 | @el.setAttributeNS(null, 'height', "100%") 16 | @el.setAttributeNS(null, 'focusable', 'false') 17 | 18 | createRect: (frame, options={}) => 19 | rect = new Rect 20 | rect.setFrame(frame) 21 | rect.setFill(options.fill) if options.fill? 22 | rect.setClipRule(options.clipRule) if options.clipRule? 23 | rect.setFillRule(options.fillRule) if options.fillRule? 24 | rect.el.classList.add(options.className) if options.className? 25 | @addSubview(rect) 26 | rect 27 | 28 | createMask: => 29 | mask = new Mask 30 | @addSubview(mask) 31 | mask 32 | 33 | createImage: => 34 | image = new Image 35 | @addSubview(image) 36 | image 37 | 38 | setMask: (mask) => 39 | @el.setAttributeNS(null, 'mask', 'url(#' + mask.name() + ')') 40 | 41 | setClipRule: (rule) => 42 | @el.setAttributeNS(null, 'clip-rule', rule) 43 | 44 | setFillRule: (rule) => 45 | @el.setAttributeNS(null, 'fill-rule', rule) 46 | 47 | maskPathCount = 0 48 | class Mask extends SVGView 49 | tag: 'mask' 50 | className: '' 51 | 52 | constructor: -> 53 | super 54 | 55 | @id = maskPathCount 56 | maskPathCount++ 57 | @el.setAttributeNS(null, 'id', @name()) 58 | 59 | name: => 60 | "mask#{@id}" 61 | 62 | class Rect extends SVGView 63 | tag: 'rect' 64 | className: '' 65 | 66 | setFrame: (@frame) => 67 | @el.setAttributeNS(null, 'x', @frame.x) 68 | @el.setAttributeNS(null, 'y', @frame.y) 69 | @el.setAttributeNS(null, 'width', @frame.width) 70 | @el.setAttributeNS(null, 'height', @frame.height) 71 | 72 | setFill: (fill) => 73 | @el.setAttributeNS(null, 'style', "fill:"+fill) 74 | 75 | class Image extends Rect 76 | tag: 'image' 77 | className: '' 78 | 79 | setURL: (href) => 80 | @el.setAttributeNS("http://www.w3.org/1999/xlink", 'href', href) 81 | 82 | module.exports = SVGView 83 | -------------------------------------------------------------------------------- /src/app/lib/scroll.coffee: -------------------------------------------------------------------------------- 1 | EventDispatcher = require('eventDispatcher') 2 | dynamics = require('dynamics') 3 | 4 | views = [] 5 | animating = false 6 | animationOptions = {} 7 | obj = null 8 | 9 | restore = (y) -> 10 | for view in views 11 | view.css(translateY: 0, translateZ: 0) 12 | window.scrollTo(0, y) 13 | document.body.style.height = "auto" 14 | scroll.scrolling = false 15 | objAnimation = null 16 | 17 | scroll = new EventDispatcher 18 | 19 | scroll.delta = 20 | x: 0 21 | y: 0 22 | 23 | scroll.value = 24 | x: 0 25 | y: 0 26 | 27 | scroll.scrolling = false 28 | 29 | window.addEventListener('scroll', -> 30 | if animating 31 | dynamics.stop(obj) 32 | restore(obj.y) 33 | animating = false 34 | animationOptions.complete?() 35 | animationOptions = {} 36 | oldValue = scroll.value 37 | scroll.value = 38 | x: window.scrollX 39 | y: window.scrollY 40 | scroll.delta = 41 | x: scroll.value.x - oldValue.x 42 | y: scroll.value.y - oldValue.y 43 | scroll.trigger('change') 44 | ) 45 | 46 | scroll.to = (options = {}) -> 47 | if animating 48 | dynamics.stop(obj) 49 | animating = false 50 | 51 | scroll.scrolling = true 52 | body = document.body 53 | html = document.documentElement 54 | 55 | bodyHeight = Math.max(body.scrollHeight, body.offsetHeight, 56 | html.clientHeight, html.scrollHeight, html.offsetHeight) 57 | document.body.style.height = bodyHeight + "px" 58 | 59 | # Max scroll value 60 | options.y = Math.min(options.y, bodyHeight - window.innerHeight) 61 | 62 | initial = window.scrollY 63 | views = options.views 64 | 65 | obj = { y: scroll.value.y } 66 | animating = true 67 | animationOptions = options 68 | dynamics.animate(obj, { 69 | y: options.y 70 | }, { 71 | type: dynamics.spring, 72 | frequency: 200, 73 | friction: Math.min(900, 500 + Math.abs(options.y - scroll.value.y) / 10), 74 | anticipationStrength: 0, 75 | anticipationSize: 0, 76 | duration: 1000, 77 | change: -> 78 | return unless scroll.scrolling 79 | for view in views 80 | view.css({ translateY: - obj.y + initial, translateZ: 0 }) 81 | scroll.value = 82 | x: scroll.value.x 83 | y: obj.y 84 | scroll.trigger('change', obj.y) 85 | complete: -> 86 | return unless scroll.scrolling 87 | options.complete?() 88 | restore(obj.y) 89 | }) 90 | 91 | module.exports = scroll 92 | -------------------------------------------------------------------------------- /src/app/controllers/app.coffee: -------------------------------------------------------------------------------- 1 | Controller = require('controller') 2 | View = require('view') 3 | LoadingView = require('loadingView') 4 | Timeline = require('timeline') 5 | Fullscreen = require('fullscreen') 6 | ForkView = require('forkView') 7 | router = require('router') 8 | config = require('config') 9 | 10 | class App extends Controller 11 | constructor: -> 12 | super 13 | 14 | config.imagesRootPath = @options.imagesRootPath 15 | 16 | @view = new View({ el: document.body, className: 'appView' }) 17 | 18 | @timeline = new Timeline 19 | @view.addSubview(@timeline.view) 20 | 21 | loadingViewContainer = new View(className: 'loadingViewContainer') 22 | @view.addSubview(loadingViewContainer) 23 | @loadingView = new LoadingView 24 | loadingViewContainer.addSubview(@loadingView) 25 | 26 | @fullscreen = new Fullscreen 27 | @fullscreen.delegate = @timeline 28 | @fullscreen.on('progress', @onFullscreenLoadingProgress) 29 | @view.addSubview(@fullscreen.view) 30 | 31 | @forkView = new ForkView 32 | @view.addSubview(@forkView) 33 | 34 | @bindEvents() 35 | 36 | bindEvents: => 37 | @timeline.on('photoClick', @onPhotoClick) 38 | @timeline.on('load', @onLoad) 39 | router.on('change', @onRouterChange) 40 | window.addEventListener('keydown', @onKeyDown) 41 | 42 | # Events 43 | onRouterChange: (state) => 44 | if state?.type == 'group' 45 | @timeline.setSelectedGroupFromPath(state.obj) 46 | else 47 | @timeline.setSelectedGroupFromPath(null) 48 | 49 | onPhotoClick: (timelineView, view, image) => 50 | @fullscreen.open(image, { 51 | view: view 52 | }) 53 | 54 | onKeyDown: (e) => 55 | if @fullscreen.hidden 56 | if e.keyCode == 37 or e.keyCode == 38 57 | @timeline.selectPrevious() 58 | e.preventDefault() 59 | e.stopPropagation() 60 | else if e.keyCode == 39 or e.keyCode == 40 61 | @timeline.selectNext() 62 | e.preventDefault() 63 | e.stopPropagation() 64 | else if e.keyCode == 32 65 | o = @timeline.currentImage() 66 | @fullscreen.open(o.image, o.options) 67 | e.preventDefault() 68 | e.stopPropagation() 69 | 70 | onLoad: => 71 | @options.onLoad?() 72 | 73 | onFullscreenLoadingProgress: (progress) => 74 | if progress == 100 75 | @loadingView.setValue(0) 76 | else 77 | @loadingView.setValue(progress / 100) 78 | 79 | module.exports = App 80 | -------------------------------------------------------------------------------- /src/app/views/timelineView.styl: -------------------------------------------------------------------------------- 1 | @import "../mixins.styl" 2 | 3 | .timelineView { 4 | width: 200px 5 | background: #fff 6 | 7 | > .curvedLinesCanvas { 8 | position: absolute 9 | top: 0 10 | right: -82px 11 | height: 100% 12 | width: 95px 13 | background: white 14 | } 15 | 16 | > .verticalLineView { 17 | position: absolute 18 | display: block 19 | top: 18px 20 | left: 187px 21 | width: 1px 22 | background: #B0B0B0 23 | transform: scale(.5) translateZ(0px) 24 | transform-origin: 0 0 25 | } 26 | 27 | > .containerView { 28 | > a, 29 | > .yearView { 30 | position: relative 31 | display: block 32 | width: 100% 33 | box-sizing: border-box 34 | text-align: right 35 | height: 36px 36 | line-height: 36px 37 | } 38 | 39 | > .yearView { 40 | color: #B0B0B0 41 | font-family: "HelveticaNeueLight", "HelveticaNeue-Light", "Helvetica-Neue-Light", "Helvetica", Arial, sans-serif 42 | font-size: 18px 43 | padding-right: 31px 44 | transform: translateZ(0px) 45 | cursor: default 46 | } 47 | 48 | > a { 49 | cursor: pointer 50 | color: #565656 51 | 52 | > span { 53 | display: inline-block 54 | vertical-align: top 55 | transform: scale(.5) translateZ(0px) 56 | transition: all .2s ease-in-out 57 | } 58 | 59 | > .textView { 60 | font-family: "HelveticaNeueLight", "HelveticaNeue-Light", "Helvetica-Neue-Light", "Helvetica", Arial, sans-serif 61 | transform-origin: 100% 50% 62 | font-size: 36px 63 | width: 200% 64 | margin-left: -200% 65 | } 66 | 67 | > .dateView { 68 | position: absolute 69 | width: 200px 70 | right: 31px 71 | left: auto 72 | top: 13px 73 | transform: none 74 | font-family: "HelveticaNeueLight", "HelveticaNeue-Light", "Helvetica-Neue-Light", "Helvetica", Arial, sans-serif 75 | font-size: 11px 76 | color: #0091FF 77 | opacity: 0 78 | transform-origin: 100% 50% 79 | transform: scale(.75) translateY(-7px) translateZ(0px) 80 | } 81 | 82 | > .circleView { 83 | width: 19px 84 | height: 19px 85 | border-radius: 19px 86 | border: 3px solid #565656 87 | background: #fff 88 | margin-top: 7px 89 | margin-left: 6px 90 | } 91 | 92 | &:hover { 93 | > .textView { 94 | color: #000 95 | transform: scale(.55) translateZ(0px) 96 | } 97 | 98 | > .circleView { 99 | border-color: #000 100 | transform: scale(.55) translateZ(0px) 101 | } 102 | } 103 | 104 | &.selected { 105 | > .textView { 106 | color: #0091FF 107 | transform: scale(.75) translateY(-7px) translateZ(0px) 108 | } 109 | 110 | > .dateView { 111 | opacity: 1 112 | transform: none 113 | } 114 | 115 | > .circleView { 116 | border-color: #0091FF 117 | transform: scale(.75) translateZ(0px) 118 | } 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/app/views/photosGroupView.coffee: -------------------------------------------------------------------------------- 1 | View = require('view') 2 | ImageView = require('imageView') 3 | ratio = require('ratio') 4 | months = require('months') 5 | config = require('config') 6 | 7 | class PhotosGroupView extends View 8 | className: 'photosGroupView' 9 | 10 | constructor: -> 11 | super 12 | @loaded = false 13 | @cacheFrame = true 14 | 15 | render: => 16 | @label = new View(tag: 'h2') 17 | @label.text(@options.group.name) 18 | @addSubview(@label) 19 | 20 | @date = new View(tag: 'span') 21 | date = new Date(@options.group.date) 22 | monthString = months[date.getMonth()].toUpperCase() 23 | @date.text("#{monthString} #{date.getFullYear()}") 24 | @label.addSubview(@date) 25 | 26 | @appendFullImage(@options.group.images[0]) 27 | @appendRowImages(@options.group.images[1..@options.group.images.length - 1]) 28 | 29 | bindEvents: => 30 | window.addEventListener('resize', @invalidate) 31 | window.addEventListener('resize', @onResize) 32 | @on('addedToDOM', @layout) 33 | 34 | appendFullImage: (image) => 35 | @fullImage = image 36 | image.view = @createImageView(image, 'full') 37 | @addSubview(image.view) 38 | 39 | appendRowImages: (images) => 40 | @images = images 41 | margins = (@images.length - 1) * 7 42 | 43 | totalWidthAt1000 = 0 44 | # Process ratios 45 | for image in @images 46 | image.ratio = image.size.width / image.size.height 47 | totalWidthAt1000 += 1000 * image.ratio 48 | for image in @images 49 | image.layout = 50 | widthPercent: 1000 * image.ratio / totalWidthAt1000 51 | 52 | # Render 53 | for i, image of @images 54 | image.view = @createImageView(image, 'row') 55 | @addSubview(image.view) 56 | image.view.el.style.width = "calc((100% - #{margins}px) * #{image.layout.widthPercent})" 57 | 58 | layout: => 59 | imageWidth = @images[0].view.width() 60 | return if !imageWidth 61 | 62 | margins = (@images.length - 1) * 7 63 | 64 | # Layout 65 | height = imageWidth / @images[0].ratio 66 | for i, image of @images 67 | image.view.el.style.height = "#{height}px" 68 | 69 | @fullImage.view.el.style.height = "#{@fullImage.size.height / @fullImage.size.width * @fullImage.view.width()}px" 70 | @invalidateCachedFrame() 71 | 72 | createImageView: (image, type) => 73 | imageRatio = if type == 'full' then ratio else "1x" 74 | filePath = image.files[imageRatio] 75 | imageView = new ImageView( 76 | className: type, 77 | queue: @options.queue, 78 | imagePath: config.imagesRootPath + filePath, 79 | object: image, 80 | loadingIndicator: true 81 | ) 82 | imageView.on('click', @onClick) 83 | imageView 84 | 85 | loadImages: => 86 | for i, image of @images 87 | image.view.load() 88 | @fullImage.view.load() 89 | 90 | setDisabled: (bool) => 91 | @el.classList.toggle('disabled', bool) 92 | for i, image of @images 93 | image.view.setDisabled(bool) 94 | @fullImage.view.setDisabled(bool) 95 | 96 | imageViewForImage: (image) => 97 | for view in @subviews 98 | return view if view.options.object == image 99 | return null 100 | 101 | # Events 102 | onClick: (imageView) => 103 | @trigger('click', @, imageView, imageView.options.object) 104 | 105 | onResize: => 106 | @setDisabled(true) 107 | clearTimeout(@resizeTimeout) if @resizeTimeout? 108 | @resizeTimeout = setTimeout(=> 109 | @setDisabled(false) 110 | @layout() 111 | , 300) 112 | 113 | module.exports = PhotosGroupView 114 | -------------------------------------------------------------------------------- /src/app/views/imageView.coffee: -------------------------------------------------------------------------------- 1 | View = require('view') 2 | SVGView = require('svgView') 3 | imageLoader = require('imageLoader') 4 | roundf = require('tools').roundf 5 | dynamics = require('dynamics') 6 | 7 | class ImageView extends View 8 | className: 'imageView' 9 | 10 | constructor: -> 11 | super 12 | 13 | @disabled = false 14 | @willLoad = false 15 | @loaded = false 16 | @loadObject = null 17 | @bindEvents() 18 | 19 | if @options.loadingIndicator 20 | @cover = new View(className: 'cover') 21 | @cover.css(visibility: 'hidden') 22 | @addSubview(@cover) 23 | @hiddenCover = true 24 | 25 | bindEvents: => 26 | @el.addEventListener('click', @onClick) 27 | 28 | load: (done) => 29 | return if @willLoad or @loaded 30 | @willLoad = true 31 | if @options.queue? 32 | @options.queue.addJob(@loadJob, { 33 | cancelled: => 34 | @willLoad = false 35 | }) 36 | else 37 | @loadJob(done) 38 | 39 | setDisabled: (bool) => 40 | return if bool == @disabled 41 | @disabled = bool 42 | if bool 43 | @el.style.backgroundImage = "none" 44 | else if @loaded 45 | @apply() 46 | 47 | showCover: => 48 | return unless @cover? 49 | if @hiddenCover 50 | @cover.css(visibility: 'visible') 51 | @hiddenCover = false 52 | 53 | setLoadingProgress: (progress) => 54 | @trigger('progress', progress) 55 | 56 | return unless @options.loadingIndicator 57 | return if @loaded 58 | 59 | if progress < 100 60 | frame = @loadingIndicatorFrame(progress) 61 | @el.style.webkitClipPath = @insetFromFrame(frame) 62 | @showCover() 63 | 64 | loadingIndicatorFrame: (progress) => 65 | frame = {} 66 | frame.width = progress / 100 * @width() * 0.3 67 | frame.height = 2 68 | frame.x = Math.round((@width() - frame.width) / 2) 69 | frame.y = Math.round((@height() - frame.height) / 2) 70 | frame 71 | 72 | insetFromFrame: (frame) => 73 | "inset(#{roundf(frame.y, 2)}px #{roundf(frame.x, 2)}px #{roundf(@height() - frame.y - frame.height, 2)}px #{roundf(@width() - frame.x - frame.width, 2)}px)" 74 | 75 | show: (done) => 76 | return unless @options.loadingIndicator 77 | 78 | frame = @loadingIndicatorFrame(100) 79 | 80 | if @visibleBounds()? 81 | @el.style.webkitClipPath = @insetFromFrame(frame) 82 | @showCover() 83 | 84 | cover = @cover 85 | frame.opacity = 1 86 | dynamics.animate(frame, { 87 | x: 0, 88 | y: 0 89 | width: @width(), 90 | height: @height(), 91 | opacity: 0 92 | }, { 93 | type: dynamics.easeInOut 94 | duration: 1000, 95 | friction: 200, 96 | change: => 97 | @el.style.webkitClipPath = @insetFromFrame(frame) 98 | cover.css(opacity: frame.opacity) 99 | complete: => 100 | cover.removeFromSuperview() 101 | done() 102 | }) 103 | else 104 | @cover.removeFromSuperview() 105 | @el.style.webkitClipPath = 'none' 106 | done() 107 | @cover = null 108 | 109 | loadJob: (done) => 110 | @loadObject = imageLoader.get(@options.imagePath) 111 | @loadObject.on('progress', => 112 | @setLoadingProgress(@loadObject.progress) 113 | ) 114 | @loadObject.on('load', => 115 | @onLoad() 116 | done() 117 | ) 118 | 119 | if @loadObject.url 120 | @onLoad() 121 | done() 122 | 123 | apply: => 124 | @el.style.backgroundImage = "url(#{@loadObject.url})" 125 | 126 | onClick: => 127 | @trigger('click', @) 128 | 129 | onLoad: => 130 | @setLoadingProgress(100) 131 | @loaded = true 132 | @apply() 133 | @show => 134 | @el.classList.add('loaded') 135 | 136 | module.exports = ImageView 137 | -------------------------------------------------------------------------------- /src/app/base/view.coffee: -------------------------------------------------------------------------------- 1 | EventDispatcher = require('eventDispatcher') 2 | scroll = require('scroll') 3 | dynamics = require('dynamics') 4 | 5 | clone = (obj) -> 6 | JSON.parse(JSON.stringify(obj)) 7 | 8 | getOffset = (el, property) -> 9 | value = 0 10 | while el? 11 | value += el[property] 12 | el = el.offsetParent 13 | value 14 | 15 | convertCoordinateToScreen = (coordinates) -> 16 | obj = clone(coordinates) 17 | obj.x = coordinates.x - scroll.value.x 18 | obj.y = coordinates.y - scroll.value.y 19 | obj 20 | 21 | getWindowSize = do -> 22 | size = {} 23 | gen = -> 24 | size = 25 | width: window.innerWidth 26 | height: window.innerHeight 27 | gen() 28 | window.addEventListener('resize', gen) 29 | -> size 30 | 31 | getRectsIntersection = (a, b) -> 32 | x = Math.max(a.x, b.x); 33 | num1 = Math.min(a.x + a.width, b.x + b.width); 34 | y = Math.max(a.y, b.y); 35 | num2 = Math.min(a.y + a.height, b.y + b.height); 36 | if num1 >= x and num2 >= y 37 | return { x: x, y: y, width: num1 - x, height: num2 - y } 38 | null 39 | 40 | getValueAndCache = (prop, fn) -> 41 | return @cachedFrameValues[prop] if @cachedFrameValues[prop]? 42 | value = fn() 43 | @cachedFrameValues[prop] = value if @cacheFrame 44 | value 45 | 46 | class View extends EventDispatcher 47 | tag: 'div' 48 | 49 | constructor: (@options = {}) -> 50 | super 51 | @el = @el or @options.el or ( 52 | if @xmlns? then document.createElementNS(@xmlns, @options['tag'] || @tag) 53 | else document.createElement(@options['tag'] || @tag) 54 | ) 55 | 56 | for className in [@className, @options.className] 57 | for c in (className ? '').split(' ') 58 | @el.classList.add(c) if c != '' 59 | 60 | @subviews = [] 61 | 62 | @cacheFrame = false 63 | @cachedFrameValues = {} 64 | 65 | @render?() 66 | @bindEvents?() 67 | @layout?() 68 | 69 | addSubview: (subview) => 70 | @el.appendChild(subview.el) 71 | subview.setSuperview(@) 72 | @subviews.push(subview) 73 | 74 | addSubviews: (subviews = []) => 75 | for subview in subviews 76 | @addSubview(subview) 77 | 78 | setSuperview: (superview) => 79 | @superview = superview 80 | if @el.parentNode? 81 | @triggerToSubviews('addedToDOM') 82 | 83 | removeFromSuperview: => 84 | @superview.el.removeChild(@el) 85 | @superview = null 86 | @triggerToSubviews('removedFromDOM') 87 | 88 | text: (text) => 89 | @el.innerHTML = text 90 | 91 | invalidateCachedFrame: => 92 | @cachedFrameValues = {} 93 | 94 | height: => 95 | getValueAndCache.call(@, 'height', => @el.clientHeight) 96 | 97 | width: => 98 | getValueAndCache.call(@, 'width', => @el.clientWidth) 99 | 100 | x: => 101 | getValueAndCache.call(@, 'x', => getOffset(@el, 'offsetLeft')) 102 | 103 | y: => 104 | getValueAndCache.call(@, 'y', => getOffset(@el, 'offsetTop')) 105 | 106 | position: => 107 | x: @x() 108 | y: @y() 109 | 110 | size: => 111 | width: @width() 112 | height: @height() 113 | 114 | frame: => 115 | x: @x() 116 | y: @y() 117 | width: @width() 118 | height: @height() 119 | 120 | screenFrame: => 121 | convertCoordinateToScreen(@frame()) 122 | 123 | visibleBounds: => 124 | windowFrame = getWindowSize() 125 | windowFrame.x = 0 126 | windowFrame.y = 0 127 | getRectsIntersection(@screenFrame(), windowFrame) 128 | 129 | isVisible: => 130 | style = window.getComputedStyle(@el) 131 | style.display != 'none' and style.visibility != 'hidden' 132 | 133 | css: => 134 | args = Array.prototype.slice.call(arguments) 135 | dynamics.css.apply(dynamics, [@el].concat(args)) 136 | 137 | animate: => 138 | args = Array.prototype.slice.call(arguments) 139 | dynamics.animate.apply(dynamics, [@el].concat(args)) 140 | 141 | module.exports = View 142 | -------------------------------------------------------------------------------- /src/app/views/photosGroupsView.coffee: -------------------------------------------------------------------------------- 1 | View = require('view') 2 | PhotosGroupView = require('photosGroupView') 3 | Queue = require('queue') 4 | 5 | class PhotosGroupsView extends View 6 | className: 'photosGroupsView' 7 | 8 | constructor: -> 9 | super 10 | 11 | @queue = new Queue 12 | @queue.maxConcurrent = 5 13 | 14 | setGroups: (groups) => 15 | for group in groups 16 | photosGroupView = new PhotosGroupView(group: group, queue: @queue) 17 | photosGroupView.on('click', @onClick) 18 | @addSubview(photosGroupView) 19 | 20 | # `anyVisibleGroup` and `visibleGroups` is a way to get the visible groups with 21 | # limited calls to `visibleBounds` which is costy 22 | anyVisibleGroup: => 23 | if @lastVisibleGroup? and @lastVisibleGroup.visibleBounds()? 24 | return @lastVisibleGroup 25 | else 26 | if @lastVisibleGroup? 27 | aroundIndex = @subviews.indexOf(@lastVisibleGroup) 28 | else 29 | aroundIndex = -1 30 | 31 | i = 1 32 | 33 | inRange = (index) => 34 | index >= 0 && index < @subviews.length 35 | checkView = (view) => 36 | visibleBounds = view.visibleBounds() 37 | if visibleBounds? 38 | @lastVisibleGroup = view 39 | return true 40 | return null 41 | 42 | while true 43 | upIndex = aroundIndex + i 44 | downIndex = aroundIndex - i 45 | 46 | upIndexInRange = inRange(upIndex) 47 | downIndexInRange = inRange(downIndex) 48 | 49 | if !upIndexInRange && !downIndexInRange 50 | return null 51 | 52 | if upIndexInRange 53 | view = @subviews[upIndex] 54 | if checkView(view)? 55 | return view 56 | 57 | if downIndexInRange 58 | view = @subviews[downIndex] 59 | if checkView(view)? 60 | return view 61 | 62 | i +=1 63 | 64 | return null 65 | 66 | loadImages: => 67 | view = @anyVisibleGroup() 68 | return unless view? 69 | 70 | @queue.cancelAllJobs() 71 | view.loadImages() 72 | 73 | i = @subviews.indexOf(view) 74 | k = 0 75 | while true 76 | k += 1 77 | next = if i + k < @subviews.length then @subviews[i + k] else null 78 | previous = if i - k >= 0 then @subviews[i - k] else null 79 | 80 | next.loadImages() if next? 81 | previous.loadImages() if previous? 82 | 83 | break if !next? and !previous? 84 | 85 | visibleGroups: => 86 | view = @anyVisibleGroup() 87 | return [] unless view? 88 | 89 | visibleGroups = [view] 90 | cachedVisibleBounds = {} 91 | 92 | index = @subviews.indexOf(view) 93 | cachedVisibleBounds[index] = view.visibleBounds() 94 | 95 | addViews = (range, incr) => 96 | k = 0 97 | cont = true 98 | 99 | checkView = (view) => 100 | if cont 101 | visibleBounds = view.visibleBounds() 102 | if visibleBounds? 103 | cachedVisibleBounds[i] = visibleBounds 104 | visibleGroups.push(view) 105 | else 106 | cont = false 107 | unless cont 108 | k += 1 109 | if k > 2 110 | return false 111 | true 112 | 113 | for i in range by incr 114 | break if i < 0 115 | break if i >= @subviews.length 116 | view = @subviews[i] 117 | if !checkView(view) 118 | break 119 | 120 | # Next views first 121 | addViews([index+1..@subviews.length-1], 1) 122 | # Then previous views 123 | addViews([index-1..0], -1) 124 | 125 | visibleGroups = visibleGroups.sort (a, b) => 126 | if @subviews.indexOf(a) > @subviews.indexOf(b) 127 | return 1 128 | else 129 | return -1 130 | 131 | return visibleGroups.map (view) => 132 | visibleBounds = cachedVisibleBounds[@subviews.indexOf(view)] 133 | { 134 | group: view.options.group, 135 | rect: visibleBounds, 136 | portion: visibleBounds.height / view.height() 137 | } 138 | 139 | groupViewY: (group) => 140 | view = @viewForGroup(group) 141 | if view? 142 | view.y() - 45 143 | else 144 | 0 145 | 146 | imageViewForImage: (image, group) => 147 | view = @viewForGroup(group) 148 | view.imageViewForImage(image) 149 | 150 | # Private 151 | viewForGroup: (group) => 152 | for view in @subviews 153 | return view if view.options.group.path == group?.path 154 | 155 | # Events 156 | onClick: (photosGroupView, view, image) => 157 | @trigger('click', @, view, image) 158 | 159 | module.exports = PhotosGroupsView 160 | -------------------------------------------------------------------------------- /src/app/controllers/timeline.coffee: -------------------------------------------------------------------------------- 1 | Controller = require('controller') 2 | View = require('view') 3 | PhotosGroupsView = require('photosGroupsView') 4 | TimelineView = require('timelineView') 5 | get = require('get') 6 | router = require('router') 7 | scroll = require('scroll') 8 | config = require('config') 9 | 10 | class Timeline extends Controller 11 | constructor: -> 12 | super 13 | 14 | @scrolling = false 15 | 16 | @view = new View(className: 'mainView') 17 | 18 | @timelineView = new TimelineView 19 | @view.addSubview(@timelineView) 20 | 21 | @photosGroupsContainerView = new View(className: 'photosGroupsContainerView') 22 | @photosGroupsView = new PhotosGroupsView 23 | 24 | @photosGroupsContainerView.addSubview(@photosGroupsView) 25 | @view.addSubview(@photosGroupsContainerView) 26 | 27 | @load() 28 | @bindEvents() 29 | 30 | load: => 31 | get config.imagesRootPath + 'info.json', @onLoad 32 | 33 | bindEvents: => 34 | @timelineView.on('click', @onClick) 35 | @timelineView.on('selectedGroupDidChange', @onSelectedGroupDidChange) 36 | @photosGroupsView.on('click', @onPhotoClick) 37 | scroll.on('change', @onScroll) 38 | window.addEventListener('resize', @onResize) 39 | 40 | setSelectedGroupFromPath: (path, options = {}) => 41 | group = @groupFromPath(path) 42 | @setSelectedGroup(group, options) 43 | 44 | setSelectedGroup: (group, options = {}) => 45 | options.animated ?= true 46 | options.directClick ?= false 47 | @timelineView.setSelectedGroup(group) 48 | @scrolling = true 49 | scroll.to( 50 | y: @photosGroupsView.groupViewY(group), 51 | views: [@photosGroupsView], 52 | animated: options.animated, 53 | complete: => 54 | @scrolling = false 55 | ) 56 | @onScroll() 57 | 58 | selectPrevious: => 59 | @setSelectedGroup(@previousGroup()) 60 | 61 | selectNext: => 62 | @setSelectedGroup(@nextGroup()) 63 | 64 | previousGroup: => 65 | if @timelineView.selectedGroup? 66 | index = @groups.indexOf(@timelineView.selectedGroup) - 1 67 | index = Math.max(0, index) 68 | else 69 | index = 0 70 | @groups[index] 71 | 72 | nextGroup: => 73 | if @timelineView.selectedGroup? 74 | index = @groups.indexOf(@timelineView.selectedGroup) + 1 75 | index = Math.min(@groups.length - 1, index) 76 | else 77 | index = 0 78 | @groups[index] 79 | 80 | currentImage: => 81 | image = @timelineView.selectedGroup.images[0] 82 | o = @images.filter (i) -> 83 | i.image == image 84 | o[0] 85 | 86 | previousImage: (image) => 87 | o = @images.filter (i) -> 88 | i.image == image 89 | index = @images.indexOf(o[0]) 90 | if index > 0 91 | index -= 1 92 | @images[index] 93 | else 94 | null 95 | 96 | nextImage: (image) => 97 | o = @images.filter (i) -> 98 | i.image == image 99 | index = @images.indexOf(o[0]) 100 | if index < @images.length - 1 101 | index += 1 102 | @images[index] 103 | else 104 | null 105 | 106 | # Private 107 | groupFromPath: (path) => 108 | return unless @groups 109 | for group in @groups 110 | return group if group.path == path 111 | 112 | updateVisibleGroups: => 113 | @timelineView.setVisibleGroups(@photosGroupsView.visibleGroups(), { 114 | autoSelect: !@scrolling 115 | }) 116 | 117 | # Events 118 | onLoad: (data) => 119 | @groups = data.groups.reverse() 120 | 121 | @photosGroupsView.setGroups(@groups) 122 | @timelineView.setGroups(@groups) 123 | 124 | @images = [] 125 | for group in @groups 126 | for image in group.images 127 | @images.push({ 128 | image: image, 129 | options: { 130 | view: @photosGroupsView.imageViewForImage(image, group) 131 | } 132 | }) 133 | 134 | if router.state?.type == 'group' 135 | @setSelectedGroupFromPath(router.state.obj, { animated: false }) 136 | else 137 | @photosGroupsView.loadImages() 138 | @updateVisibleGroups() 139 | @trigger('load') 140 | 141 | onScroll: => 142 | requestAnimationFrame (t) => 143 | @photosGroupsView.loadImages() 144 | @updateVisibleGroups() 145 | 146 | onResize: => 147 | requestAnimationFrame (t) => 148 | @updateVisibleGroups() 149 | 150 | onClick: (group) => 151 | router.goToGroup(group) 152 | 153 | onSelectedGroupDidChange: (group) => 154 | # return if scroll.scrolling 155 | # router.goToGroup(group, { trigger: false }) 156 | 157 | onPhotoClick: (photosGroupView, view, image) => 158 | @trigger('photoClick', @, view, image) 159 | 160 | module.exports = Timeline 161 | -------------------------------------------------------------------------------- /scripts/prepare.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ./node_modules/coffee-script/bin/coffee 2 | 3 | easyimage = require 'easyimage' 4 | syncfs = require 'fs' 5 | fs = require 'q-io/fs' 6 | Q = require 'q' 7 | _ = require 'lodash' 8 | 9 | BASE = './' 10 | SRC = "#{BASE}data/" 11 | DST = "#{BASE}public/data/" 12 | 13 | queueArray = [] 14 | executingQueue = false 15 | job = 0 16 | totalJobs = 0 17 | queue = (fn) -> 18 | totalJobs += 1 19 | deferred = Q.defer() 20 | queueArray.push( 21 | -> 22 | fn().then -> 23 | deferred.resolve.apply(deferred, Array.prototype.slice.call(arguments)) 24 | nextInQueue() 25 | ) 26 | nextInQueue() if !executingQueue 27 | deferred.promise 28 | 29 | nextInQueue = -> 30 | if queueArray.length == 0 31 | executingQueue = false 32 | return 33 | 34 | job += 1 35 | console.log 'Start #' + job + ' / ' + totalJobs 36 | executingQueue = true 37 | fn = queueArray.splice(0, 1) 38 | fn[0]() 39 | 40 | clone = (obj) -> 41 | JSON.parse(JSON.stringify(obj)) 42 | 43 | start = -> 44 | fs.list(SRC).then((dirs) -> 45 | console.log('Finding info.json files') 46 | Q.all do -> 47 | for dir in dirs 48 | continue if dir == '.DS_Store' 49 | continue unless existsInfoJson(dir) 50 | parseGroup(dir) 51 | ).then((groups) -> 52 | console.log('Creating timeline...') 53 | createTimeline(groups) 54 | ).then(-> 55 | console.log('Done!') 56 | ).catch((e) -> 57 | console.log(e) 58 | ) 59 | 60 | existsInfoJson = (dir) -> 61 | groupDir = SRC + dir + "/" 62 | syncfs.existsSync(groupDir + "info.json") 63 | 64 | parseGroup = (dir) -> 65 | groupDir = SRC + dir + "/" 66 | dstGroupDir = DST + dir + "/" 67 | fs.exists(dstGroupDir) 68 | .then((exists) -> 69 | return if exists 70 | fs.makeDirectory(dstGroupDir) 71 | ).then(-> 72 | console.log "Parsing #{groupDir + "info.json"}" 73 | parseInfoFile(groupDir, "info.json") 74 | ).then((json) -> 75 | ps = [] 76 | console.log "Writing #{dstGroupDir + "info.json"}" 77 | ps.push(fs.write(dstGroupDir + "info.json", JSON.stringify(json))) 78 | 79 | json.path = dir + "/" 80 | 81 | Q.all(ps).then(-> json) 82 | ) 83 | 84 | parseInfoFile = (groupDir, fileName, dstDir) -> 85 | infoFilePath = groupDir + fileName 86 | returnJson = null 87 | fs.read(infoFilePath).then((data) -> 88 | JSON.parse(data) 89 | ) 90 | 91 | createTimeline = (groups) -> 92 | json = {} 93 | json.groups = groups.sort (a, b) -> 94 | if (new Date(a.date)).getTime() > (new Date(b.date)).getTime() 95 | return 1 96 | else 97 | return -1 98 | 99 | Q.all( 100 | _.flatten do -> 101 | for group in json.groups 102 | images = group.images 103 | group.images = [] 104 | for i in [0..(images.length - 1)] 105 | image = images[i] 106 | group.images.push(image) 107 | do (group, image) -> 108 | processTimelineImage(group, image).then((args) -> 109 | delete image.file 110 | image.files = _.mapValues(args.files, (file) -> "#{group.path}#{file}" ) 111 | image.size = 112 | width: args.info.width 113 | height: args.info.height 114 | ) 115 | ).then(-> 116 | console.log "Writing #{DST + "info.json"}" 117 | fs.write(DST + "info.json", JSON.stringify(json)) 118 | ) 119 | 120 | processTimelineImage = (group, image) -> 121 | srcFile = SRC + group.path + image.file 122 | dstPath = DST + group.path 123 | files = [] 124 | resizeImage(srcFile, dstPath, { 125 | width: 750 126 | }, { 127 | suffix: '_timeline' 128 | }).then((filenames) -> 129 | files = _.merge.apply(this, filenames) 130 | queue => 131 | easyimage.info(dstPath + files['1x']) 132 | ).then((info) -> 133 | files: files, info: info 134 | ) 135 | 136 | resizeImage = (file, dstPath, opts = {}, others = {}) => 137 | Q.all do -> 138 | for ratio in [1, 2] 139 | do (ratio) -> 140 | others.suffix ?= '' 141 | options = clone(opts) 142 | suffix = if ratio == 1 then '' else "@#{ratio}x" 143 | fileArgs = file.split('/') 144 | filename = fileArgs[fileArgs.length - 1] 145 | filenameArgs = filename.split('.') 146 | suffixedFilename = '' 147 | for i, arg of filenameArgs 148 | if parseInt(i) == filenameArgs.length - 1 149 | suffixedFilename += others.suffix + suffix 150 | if i > 0 151 | suffixedFilename += '.' 152 | suffixedFilename += arg 153 | 154 | options.src = file 155 | options.dst = dstPath + suffixedFilename 156 | options.quality = 95 157 | for key in ['width', 'height', 'cropwidth', 'cropheight', 'x', 'y'] 158 | options[key] *= ratio if options[key]? 159 | 160 | fs.exists(options.dst) 161 | .then((exists) -> 162 | return if exists 163 | queue => 164 | console.log "Resizing #{options.src} to #{options.dst}" 165 | return Q() if syncfs.existsSync(options.dst) 166 | easyimage.resize(options).catch((e) -> 167 | console.log(e) 168 | ) 169 | ).then(-> 170 | r = {} 171 | r["#{ratio}x"] = suffixedFilename 172 | r 173 | ) 174 | 175 | start() 176 | -------------------------------------------------------------------------------- /src/app/controllers/fullscreen.coffee: -------------------------------------------------------------------------------- 1 | Controller = require('controller') 2 | View = require('view') 3 | ImageView = require('imageView') 4 | ratio = require('ratio') 5 | config = require('config') 6 | scroll = require('scroll') 7 | dynamics = require('dynamics') 8 | tools = require('tools') 9 | 10 | springOptions = { 11 | type: dynamics.spring, 12 | frequency: 200, 13 | friction: 500, 14 | anticipationStrength: 0, 15 | anticipationSize: 0, 16 | duration: 1000 17 | } 18 | 19 | class Fullscreen extends Controller 20 | constructor: -> 21 | super 22 | 23 | @view = new View(className: 'fullscreenView') 24 | @view.css(visibility: 'hidden') 25 | @view.el.addEventListener('click', @onClick) 26 | 27 | @backgroundView = new View(className: 'backgroundView') 28 | @backgroundView.css(opacity: 0) 29 | @view.addSubview(@backgroundView) 30 | 31 | @imageView = null 32 | @originalView = null 33 | @hidden = true 34 | 35 | open: (image, options = {}) => 36 | return unless @hidden 37 | @hidden = false 38 | @image = image 39 | filePath = image.files[ratio] 40 | 41 | @imageView?.css( 42 | zIndex: 9 43 | ) 44 | 45 | @imageView = new ImageView( 46 | imagePath: config.imagesRootPath + filePath, 47 | object: image 48 | ) 49 | 50 | @originalView = options.view 51 | 52 | props = @scaleAndTranslate(@view, @originalView) 53 | props.left = 0 54 | props.top = 0 55 | props.width = @view.width() 56 | props.height = @view.height() 57 | props.transformOrigin = "0 0" 58 | 59 | @imageView.css(props) 60 | 61 | @applyProgress(@imageView) 62 | 63 | @loading = true 64 | @imageView.load => 65 | @loading = false 66 | @trigger('progress', 0) 67 | @view.addSubview(@imageView) 68 | @originalView.css(visibility: 'hidden') 69 | @view.css(visibility: 'visible') 70 | 71 | @backgroundView.animate({ 72 | opacity: 1 73 | }, { 74 | type: dynamics.easeInOut, 75 | duration: 200 76 | }) 77 | 78 | @imageView.animate({ 79 | scaleX: 1, 80 | scaleY: 1, 81 | translateX: 0, 82 | translateY: 0 83 | }, springOptions) 84 | 85 | window.addEventListener('resize', @layout) 86 | window.addEventListener('keydown', @onKeyDown) 87 | 88 | slide: (image, options={}) => 89 | return if @hidden 90 | return if @loading 91 | 92 | oldImageView = @imageView 93 | 94 | @image = image 95 | filePath = image.files[ratio] 96 | @originalView?.css(visibility: 'visible') 97 | @originalView = options.view 98 | @imageView = imageView = new ImageView( 99 | imagePath: config.imagesRootPath + filePath, 100 | object: image 101 | ) 102 | @imageView.css({ 103 | left: 0, 104 | top: 0, 105 | width: @view.width(), 106 | height: @view.height(), 107 | translateX: options.direction * @view.width(), 108 | transformOrigin: "0 0" 109 | }) 110 | 111 | oldImageView.animate({ 112 | translateX: -options.direction * @view.width() 113 | }, tools.merge(springOptions, { 114 | complete: => 115 | oldImageView.removeFromSuperview() 116 | })) 117 | 118 | @applyProgress(@imageView) 119 | @loading = true 120 | @imageView.load => 121 | @loading = false 122 | @view.addSubview(imageView) 123 | options.view.css(visibility: 'hidden') 124 | imageView.animate({ 125 | translateX: 0 126 | }, springOptions) 127 | 128 | bounce: (direction) => 129 | @imageView.css(translateX: 0) 130 | @imageView.animate({ 131 | translateX: -direction * 100 132 | }, { 133 | type: dynamics.bounce, 134 | frequency: 200, 135 | friction: 200, 136 | duration: 700 137 | }) 138 | 139 | layout: => 140 | @imageView.css({ 141 | width: @view.width(), 142 | height: @view.height() 143 | }) 144 | 145 | close: => 146 | return if @hidden 147 | window.removeEventListener('resize', @layout) 148 | window.removeEventListener('keydown', @onKeyDown) 149 | @hidden = true 150 | @loading = false 151 | 152 | originalView = @originalView 153 | 154 | props = @scaleAndTranslate(@view, @originalView) 155 | 156 | body = new View(el: document.body) 157 | @imageView.el.classList.add('mainImageView') 158 | body.addSubview(@imageView) 159 | @imageView.css({ 160 | top: scroll.value.y 161 | }) 162 | 163 | imageView = @imageView 164 | @imageView.animate(props, tools.merge(springOptions, { 165 | complete: => 166 | if @hidden || originalView != @originalView 167 | originalView.css(visibility: 'visible') 168 | imageView.removeFromSuperview() 169 | if @hidden 170 | @view.css(visibility: 'hidden') 171 | })) 172 | 173 | @backgroundView.animate({ 174 | opacity: 0 175 | }, { 176 | type: dynamics.easeInOut, 177 | duration: 200 178 | }) 179 | 180 | previous: => 181 | return if @time and @time > Date.now() - 200 182 | @time = Date.now() 183 | o = @delegate?.previousImage(@image) 184 | if o? 185 | o.options.direction = -1 186 | @slide(o.image, o.options) 187 | else 188 | @bounce(-1) 189 | 190 | next: => 191 | return if @time and @time > Date.now() - 200 192 | @time = Date.now() 193 | o = @delegate?.nextImage(@image) 194 | if o? 195 | o.options.direction = 1 196 | @slide(o.image, o.options) 197 | else 198 | @bounce(1) 199 | 200 | applyProgress: (imageView) => 201 | imageView.on 'progress', (progress) => 202 | @trigger('progress', progress) 203 | 204 | scaleAndTranslate: (viewA, viewB) => 205 | frame = viewB.screenFrame() 206 | 207 | ratioOriginal = frame.width / frame.height 208 | ratioView = viewA.width() / viewA.height() 209 | scale = 0 210 | 211 | translateX = frame.x 212 | translateY = frame.y 213 | 214 | if ratioOriginal > ratioView 215 | scale = frame.width / viewA.width() 216 | translateY += (frame.height / scale - viewA.height()) / 2 * scale 217 | else 218 | scale = frame.height / viewA.height() 219 | translateX += (frame.width / scale - viewA.width()) / 2 * scale 220 | 221 | { 222 | translateX: translateX, 223 | translateY: translateY, 224 | scaleX: scale, 225 | scaleY: scale, 226 | } 227 | 228 | # Events 229 | onClick: => 230 | @close() 231 | 232 | onKeyDown: (e) => 233 | if e.keyCode == 27 or e.keyCode == 32 234 | @close() 235 | e.preventDefault() 236 | e.stopPropagation() 237 | else if e.keyCode == 39 or e.keyCode == 40 238 | @next() 239 | e.preventDefault() 240 | e.stopPropagation() 241 | else if e.keyCode == 37 or e.keyCode == 38 242 | @previous() 243 | e.preventDefault() 244 | e.stopPropagation() 245 | 246 | module.exports = Fullscreen 247 | -------------------------------------------------------------------------------- /src/app/views/timelineView.coffee: -------------------------------------------------------------------------------- 1 | View = require('view') 2 | scroll = require('scroll') 3 | months = require('months') 4 | roundf = require('tools').roundf 5 | 6 | pixelRatio = window.devicePixelRatio ? 1 7 | 8 | multiply = (obj, value) -> 9 | newObj = {} 10 | for k, v of obj 11 | newObj[k] = v * value 12 | newObj 13 | 14 | getWindowSize = do -> 15 | size = {} 16 | gen = -> 17 | size = 18 | width: window.innerWidth 19 | height: window.innerHeight 20 | gen() 21 | window.addEventListener('resize', gen) 22 | -> size 23 | 24 | class TimelineView extends View 25 | className: 'timelineView' 26 | 27 | constructor: -> 28 | super 29 | 30 | @translateY = 0 31 | @canvas = new View(tag: 'canvas', className: 'curvedLinesCanvas') 32 | @ctx = @canvas.el.getContext("2d") 33 | @addSubview(@canvas) 34 | 35 | @verticalLineView = new View(tag: 'span', className: 'verticalLineView') 36 | @addSubview(@verticalLineView) 37 | 38 | @containerView = new View(className: 'containerView') 39 | @addSubview(@containerView) 40 | 41 | @selectedGroup = null 42 | @groups = [] 43 | 44 | window.addEventListener('resize', @onResize) 45 | window.addEventListener('load', => 46 | @onLoad() 47 | @updateCanvasSize() 48 | @center() 49 | @redraw() 50 | ) 51 | scroll.on('change', @onScroll) 52 | 53 | setVisibleGroups: (groups, options={}) => 54 | options.autoSelect ?= true 55 | 56 | groups.sort (a, b) -> 57 | a.rect.y > b.rect.y 58 | @visibleGroups = groups 59 | 60 | selectedGroup = null 61 | maxPortion = 0 62 | maxGroup = null 63 | for group in groups 64 | if group.portion > 0.66 65 | selectedGroup = group.group 66 | break 67 | 68 | if group.portion > maxPortion 69 | maxPortion = group.portion 70 | maxGroup = group.group 71 | 72 | selectedGroup = maxGroup if !selectedGroup 73 | 74 | if @selectedGroup != selectedGroup && options.autoSelect 75 | @setSelectedGroup(selectedGroup) 76 | @trigger('selectedGroupDidChange', selectedGroup) 77 | 78 | @redraw() 79 | 80 | setSelectedGroup: (group, timeout=null) => 81 | if @selectedGroup != group 82 | clearTimeout(@selectedGroupTimeout) if @selectedGroupTimeout? 83 | @selectedGroupTimeout = null 84 | 85 | @selectedGroup = group 86 | 87 | if @selectedGroup 88 | item = @itemForGroup(@selectedGroup) 89 | if item and item != @selectedItem 90 | @selectedItem?.el.classList.remove('selected') 91 | if !timeout? 92 | item.el.classList.add('selected') 93 | else 94 | @selectedGroupTimeout = setTimeout => 95 | item.el.classList.add('selected') 96 | , timeout 97 | @selectedItem = item 98 | else 99 | @selectedItem?.el.classList.remove('selected') 100 | @selectedItem = null 101 | 102 | setGroups: (groups) => 103 | @groups = groups 104 | currentYear = null 105 | verticalLineViewHeight = 0 106 | 107 | addYearView = (year) => 108 | yearView = new View(tag: 'p', className: 'yearView') 109 | yearView.text(year) 110 | @containerView.addSubview(yearView) 111 | 112 | for group in groups 113 | do (group) => 114 | date = new Date(group.date) 115 | if currentYear? and currentYear != date.getFullYear() 116 | verticalLineViewHeight += 36 117 | addYearView(currentYear) 118 | 119 | itemView = new View(tag: 'a', group: group) 120 | itemView.cacheFrame = true 121 | itemView.el.addEventListener('click', => 122 | @trigger('click', group) 123 | ) 124 | 125 | textView = new View(tag: 'span', className: 'textView') 126 | textView.text(group.name) 127 | itemView.addSubview(textView) 128 | 129 | dateView = new View(tag: 'span', className: 'dateView') 130 | 131 | monthString = months[date.getMonth()].toUpperCase() 132 | 133 | dateView.text("#{monthString} #{date.getFullYear()}") 134 | itemView.addSubview(dateView) 135 | 136 | circleView = new View(tag: 'span', className: 'circleView') 137 | itemView.addSubview(circleView) 138 | 139 | verticalLineViewHeight += 36 140 | 141 | @containerView.addSubview(itemView) 142 | 143 | currentYear = date.getFullYear() 144 | 145 | addYearView(currentYear) if currentYear? 146 | 147 | @verticalLineView.el.style.height = (verticalLineViewHeight - 36) + "px" 148 | 149 | itemForGroup: (group) => 150 | for view in @containerView.subviews 151 | continue if !view.options.group? 152 | return view if view.options.group.path == group.path 153 | null 154 | 155 | updateCanvasSize: => 156 | @canvas.el.width = 95 * pixelRatio 157 | @canvas.el.height = @canvas.height() * pixelRatio 158 | 159 | center: => 160 | height = @height() 161 | @marginTop = height * 0.4 162 | @marginTop = Math.max(16, @marginTop) 163 | @containerView.el.style.marginTop = "#{@marginTop}px" 164 | @verticalLineView.el.style.marginTop = "#{@marginTop}px" 165 | 166 | redraw: => 167 | requestAnimationFrame => 168 | @draw(@ctx) 169 | 170 | draw: (ctx) => 171 | if !@isVisible() 172 | return 173 | 174 | canvasWidth = @canvas.el.width / pixelRatio 175 | canvasHeight = @canvas.el.height / pixelRatio 176 | 177 | ctx.fillStyle = 'white' 178 | if @lastDrawnRect? 179 | ctx.fillRect.apply(ctx, @lastDrawnRect) 180 | else 181 | ctx.fillRect(0, 0, canvasWidth * pixelRatio, canvasHeight * pixelRatio) 182 | 183 | if !@visibleGroups? or @visibleGroups.length == 0 184 | @lastDrawnRect = [0,0,0,0] 185 | return 186 | 187 | fullRect = null 188 | for group in @visibleGroups 189 | item = @itemForGroup(group.group) 190 | continue unless item? 191 | 192 | itemRect = item.frame() 193 | itemRect.y += @translateY 194 | groupRect = group.rect 195 | 196 | if group.group == @selectedGroup 197 | ctx.strokeStyle = '#0091FF' 198 | ctx.lineWidth = '3' 199 | else 200 | ctx.strokeStyle = "rgba(176, 176, 176, #{Math.min(1, group.portion * 4)})" 201 | ctx.lineWidth = '1' 202 | 203 | y1 = itemRect.y + itemRect.height / 2 204 | y2 = groupRect.y + groupRect.height / 2 205 | minY = Math.min(y1, y2) - 5 206 | maxY = Math.max(y1, y2) + 5 207 | rect = [0, minY * pixelRatio, 95 * pixelRatio, (maxY - minY) * pixelRatio ] 208 | 209 | if fullRect? 210 | newHeight = Math.max(fullRect[3] + fullRect[1], rect[3] + rect[1]) - Math.min(fullRect[1], rect[1]) 211 | minY = Math.min(fullRect[1], rect[1]) 212 | fullRect = [fullRect[0], minY, rect[2], newHeight] 213 | else 214 | fullRect = rect 215 | 216 | @drawLine(ctx, { x: 0, y: y1 }, { x: 95, y: y2 }) 217 | 218 | @lastDrawnRect = fullRect 219 | 220 | drawLine: (ctx, from, to) => 221 | from = multiply(from, pixelRatio) 222 | to = multiply(to, pixelRatio) 223 | midX = (from.x + to.x) / 2 + from.x 224 | ctx.beginPath() 225 | ctx.moveTo(from.x, from.y) 226 | ctx.bezierCurveTo(midX, from.y, midX, to.y, to.x, to.y) 227 | ctx.stroke() 228 | ctx.closePath() 229 | 230 | # Events 231 | onLoad: => 232 | for view in @containerView.subviews 233 | view.invalidateCachedFrame() 234 | 235 | onResize: => 236 | @updateCanvasSize() 237 | @center() 238 | @redraw() 239 | @scrollDimensions = null 240 | for view in @containerView.subviews 241 | view.invalidateCachedFrame() 242 | 243 | onScroll: => 244 | if !@isVisible() 245 | return 246 | 247 | if !@scrollDimensions? 248 | @scrollDimensions = 249 | bodyHeight: document.body.clientHeight 250 | windowHeight: getWindowSize().height 251 | containerHeight: @containerView.height() 252 | 253 | scrollY = scroll.value.y 254 | height = @scrollDimensions.bodyHeight - @scrollDimensions.windowHeight 255 | percent = scrollY / height 256 | 257 | @translateY = roundf(-percent * (@scrollDimensions.containerHeight - @marginTop), 2) 258 | transform = "translateY("+@translateY+"px)" 259 | 260 | @containerView.el.style.webkitTransform = transform 261 | @verticalLineView.el.style.webkitTransform = transform 262 | 263 | module.exports = TimelineView 264 | -------------------------------------------------------------------------------- /src/app/vendor/dynamics.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | (function() { 3 | var Color, DecomposedMatrix, DecomposedMatrix2D, InterpolableArray, InterpolableColor, InterpolableNumber, InterpolableObject, InterpolableString, Matrix, Matrix2D, Set, Vector, addTimeout, animationTick, animations, animationsTimeouts, applyDefaults, applyFrame, applyProperties, baseSVG, cacheFn, cancelTimeout, clone, createInterpolable, defaultValueForKey, degProperties, dynamics, getCurrentProperties, interpolate, isDocumentVisible, isSVGElement, lastTime, leftDelayForTimeout, makeArrayFn, observeVisibilityChange, parseProperties, prefixFor, propertyWithPrefix, pxProperties, rAF, roundf, runLoopPaused, runLoopRunning, runloopTick, runloops, setRealTimeout, slow, slowRatio, startAnimation, startMainRunLoop, svgProperties, tick, timeBeforeVisibilityChange, timeoutLastId, timeouts, toDashed, transformProperties, transformValueForProperty, unitForProperty, 4 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 5 | 6 | isDocumentVisible = function() { 7 | return document.visibilityState === "visible" || (dynamics.tests != null); 8 | }; 9 | 10 | observeVisibilityChange = (function() { 11 | var fns; 12 | fns = []; 13 | if (typeof document !== "undefined" && document !== null) { 14 | document.addEventListener("visibilitychange", function() { 15 | var fn, _i, _len, _results; 16 | _results = []; 17 | for (_i = 0, _len = fns.length; _i < _len; _i++) { 18 | fn = fns[_i]; 19 | _results.push(fn(isDocumentVisible())); 20 | } 21 | return _results; 22 | }); 23 | } 24 | return function(fn) { 25 | return fns.push(fn); 26 | }; 27 | })(); 28 | 29 | clone = function(o) { 30 | var k, newO, v; 31 | newO = {}; 32 | for (k in o) { 33 | v = o[k]; 34 | newO[k] = v; 35 | } 36 | return newO; 37 | }; 38 | 39 | cacheFn = function(func) { 40 | var data; 41 | data = {}; 42 | return function() { 43 | var k, key, result, _i, _len; 44 | key = ""; 45 | for (_i = 0, _len = arguments.length; _i < _len; _i++) { 46 | k = arguments[_i]; 47 | key += k.toString() + ","; 48 | } 49 | result = data[key]; 50 | if (!result) { 51 | data[key] = result = func.apply(this, arguments); 52 | } 53 | return result; 54 | }; 55 | }; 56 | 57 | makeArrayFn = function(fn) { 58 | return function(el) { 59 | var args, i, res; 60 | if (el instanceof Array || el instanceof NodeList || el instanceof HTMLCollection) { 61 | res = (function() { 62 | var _i, _ref, _results; 63 | _results = []; 64 | for (i = _i = 0, _ref = el.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 65 | args = Array.prototype.slice.call(arguments, 1); 66 | args.splice(0, 0, el[i]); 67 | _results.push(fn.apply(this, args)); 68 | } 69 | return _results; 70 | }).apply(this, arguments); 71 | return res; 72 | } 73 | return fn.apply(this, arguments); 74 | }; 75 | }; 76 | 77 | applyDefaults = function(options, defaults) { 78 | var k, v, _results; 79 | _results = []; 80 | for (k in defaults) { 81 | v = defaults[k]; 82 | _results.push(options[k] != null ? options[k] : options[k] = v); 83 | } 84 | return _results; 85 | }; 86 | 87 | applyFrame = function(el, properties) { 88 | var k, v, _results; 89 | if ((el.style != null)) { 90 | return applyProperties(el, properties); 91 | } else { 92 | _results = []; 93 | for (k in properties) { 94 | v = properties[k]; 95 | _results.push(el[k] = v.format()); 96 | } 97 | return _results; 98 | } 99 | }; 100 | 101 | applyProperties = function(el, properties) { 102 | var isSVG, k, matrix, transforms, v; 103 | properties = parseProperties(properties); 104 | transforms = []; 105 | isSVG = isSVGElement(el); 106 | for (k in properties) { 107 | v = properties[k]; 108 | if (transformProperties.contains(k)) { 109 | transforms.push([k, v]); 110 | } else { 111 | if (v.format != null) { 112 | v = v.format(); 113 | } 114 | if (typeof v === 'number') { 115 | v = "" + v + (unitForProperty(k, v)); 116 | } 117 | if (isSVG && svgProperties.contains(k)) { 118 | el.setAttribute(k, v); 119 | } else { 120 | el.style[propertyWithPrefix(k)] = v; 121 | } 122 | } 123 | } 124 | if (transforms.length > 0) { 125 | if (isSVG) { 126 | matrix = new Matrix2D(); 127 | matrix.applyProperties(transforms); 128 | return el.setAttribute("transform", matrix.decompose().format()); 129 | } else { 130 | v = (transforms.map(function(transform) { 131 | return transformValueForProperty(transform[0], transform[1]); 132 | })).join(" "); 133 | return el.style[propertyWithPrefix("transform")] = v; 134 | } 135 | } 136 | }; 137 | 138 | isSVGElement = function(el) { 139 | var _ref, _ref1; 140 | if ((typeof SVGElement !== "undefined" && SVGElement !== null) && (typeof SVGSVGElement !== "undefined" && SVGSVGElement !== null)) { 141 | return el instanceof SVGElement && !(el instanceof SVGSVGElement); 142 | } else { 143 | return (_ref = (_ref1 = dynamics.tests) != null ? typeof _ref1.isSVG === "function" ? _ref1.isSVG(el) : void 0 : void 0) != null ? _ref : false; 144 | } 145 | }; 146 | 147 | roundf = function(v, decimal) { 148 | var d; 149 | d = Math.pow(10, decimal); 150 | return Math.round(v * d) / d; 151 | }; 152 | 153 | Set = (function() { 154 | function Set(array) { 155 | var v, _i, _len; 156 | this.obj = {}; 157 | for (_i = 0, _len = array.length; _i < _len; _i++) { 158 | v = array[_i]; 159 | this.obj[v] = 1; 160 | } 161 | } 162 | 163 | Set.prototype.contains = function(v) { 164 | return this.obj[v] === 1; 165 | }; 166 | 167 | return Set; 168 | 169 | })(); 170 | 171 | toDashed = function(str) { 172 | return str.replace(/([A-Z])/g, function($1) { 173 | return "-" + $1.toLowerCase(); 174 | }); 175 | }; 176 | 177 | pxProperties = new Set('marginTop,marginLeft,marginBottom,marginRight,paddingTop,paddingLeft,paddingBottom,paddingRight,top,left,bottom,right,translateX,translateY,translateZ,perspectiveX,perspectiveY,perspectiveZ,width,height,maxWidth,maxHeight,minWidth,minHeight,borderRadius'.split(',')); 178 | 179 | degProperties = new Set('rotate,rotateX,rotateY,rotateZ,skew,skewX,skewY,skewZ'.split(',')); 180 | 181 | transformProperties = new Set('translate,translateX,translateY,translateZ,scale,scaleX,scaleY,scaleZ,rotate,rotateX,rotateY,rotateZ,rotateC,rotateCX,rotateCY,skew,skewX,skewY,skewZ,perspective'.split(',')); 182 | 183 | svgProperties = new Set('accent-height,ascent,azimuth,baseFrequency,baseline-shift,bias,cx,cy,d,diffuseConstant,divisor,dx,dy,elevation,filterRes,fx,fy,gradientTransform,height,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,letter-spacing,limitingConeAngle,markerHeight,markerWidth,numOctaves,order,overline-position,overline-thickness,pathLength,points,pointsAtX,pointsAtY,pointsAtZ,r,radius,rx,ry,seed,specularConstant,specularExponent,stdDeviation,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,surfaceScale,target,targetX,targetY,transform,underline-position,underline-thickness,viewBox,width,x,x1,x2,y,y1,y2,z'.split(',')); 184 | 185 | unitForProperty = function(k, v) { 186 | if (typeof v !== 'number') { 187 | return ''; 188 | } 189 | if (pxProperties.contains(k)) { 190 | return 'px'; 191 | } else if (degProperties.contains(k)) { 192 | return 'deg'; 193 | } 194 | return ''; 195 | }; 196 | 197 | transformValueForProperty = function(k, v) { 198 | var match, unit; 199 | match = ("" + v).match(/^([0-9.-]*)([^0-9]*)$/); 200 | if (match != null) { 201 | v = match[1]; 202 | unit = match[2]; 203 | } else { 204 | v = parseFloat(v); 205 | } 206 | v = roundf(parseFloat(v), 10); 207 | if ((unit == null) || unit === "") { 208 | unit = unitForProperty(k, v); 209 | } 210 | return "" + k + "(" + v + unit + ")"; 211 | }; 212 | 213 | parseProperties = function(properties) { 214 | var axis, match, parsed, property, value, _i, _len, _ref; 215 | parsed = {}; 216 | for (property in properties) { 217 | value = properties[property]; 218 | if (transformProperties.contains(property)) { 219 | match = property.match(/(translate|rotateC|rotate|skew|scale|perspective)(X|Y|Z|)/); 220 | if (match && match[2].length > 0) { 221 | parsed[property] = value; 222 | } else { 223 | _ref = ['X', 'Y', 'Z']; 224 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 225 | axis = _ref[_i]; 226 | parsed[match[1] + axis] = value; 227 | } 228 | } 229 | } else { 230 | parsed[property] = value; 231 | } 232 | } 233 | return parsed; 234 | }; 235 | 236 | defaultValueForKey = function(key) { 237 | var v; 238 | v = key === 'opacity' ? 1 : 0; 239 | return "" + v + (unitForProperty(key, v)); 240 | }; 241 | 242 | getCurrentProperties = function(el, keys) { 243 | var isSVG, key, matrix, properties, style, v, _i, _j, _len, _len1, _ref; 244 | properties = {}; 245 | isSVG = isSVGElement(el); 246 | if (el.style != null) { 247 | style = window.getComputedStyle(el, null); 248 | for (_i = 0, _len = keys.length; _i < _len; _i++) { 249 | key = keys[_i]; 250 | if (transformProperties.contains(key)) { 251 | if (properties['transform'] == null) { 252 | if (isSVG) { 253 | matrix = new Matrix2D((_ref = el.transform.baseVal.consolidate()) != null ? _ref.matrix : void 0); 254 | } else { 255 | matrix = Matrix.fromTransform(style[propertyWithPrefix('transform')]); 256 | } 257 | properties['transform'] = matrix.decompose(); 258 | } 259 | } else { 260 | v = style[key]; 261 | if ((v == null) && svgProperties.contains(key)) { 262 | v = el.getAttribute(key); 263 | } 264 | if (v === "" || (v == null)) { 265 | v = defaultValueForKey(key); 266 | } 267 | properties[key] = createInterpolable(v); 268 | } 269 | } 270 | } else { 271 | for (_j = 0, _len1 = keys.length; _j < _len1; _j++) { 272 | key = keys[_j]; 273 | properties[key] = createInterpolable(el[key]); 274 | } 275 | } 276 | return properties; 277 | }; 278 | 279 | createInterpolable = function(value) { 280 | var interpolable, klass, klasses, _i, _len; 281 | klasses = [InterpolableArray, InterpolableObject, InterpolableNumber, InterpolableString]; 282 | for (_i = 0, _len = klasses.length; _i < _len; _i++) { 283 | klass = klasses[_i]; 284 | interpolable = klass.create(value); 285 | if (interpolable != null) { 286 | return interpolable; 287 | } 288 | } 289 | return null; 290 | }; 291 | 292 | InterpolableString = (function() { 293 | function InterpolableString(parts) { 294 | this.parts = parts; 295 | this.format = __bind(this.format, this); 296 | this.interpolate = __bind(this.interpolate, this); 297 | } 298 | 299 | InterpolableString.prototype.interpolate = function(endInterpolable, t) { 300 | var end, i, newParts, start, _i, _ref; 301 | start = this.parts; 302 | end = endInterpolable.parts; 303 | newParts = []; 304 | for (i = _i = 0, _ref = Math.min(start.length, end.length); 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 305 | if (start[i].interpolate != null) { 306 | newParts.push(start[i].interpolate(end[i], t)); 307 | } else { 308 | newParts.push(start[i]); 309 | } 310 | } 311 | return new InterpolableString(newParts); 312 | }; 313 | 314 | InterpolableString.prototype.format = function() { 315 | var parts; 316 | parts = this.parts.map(function(val) { 317 | if (val.format != null) { 318 | return val.format(); 319 | } else { 320 | return val; 321 | } 322 | }); 323 | return parts.join(''); 324 | }; 325 | 326 | InterpolableString.create = function(value) { 327 | var index, match, matches, parts, re, type, types, _i, _j, _len, _len1; 328 | value = "" + value; 329 | matches = []; 330 | types = [ 331 | { 332 | re: /(#[a-f\d]{3,6})/ig, 333 | klass: InterpolableColor, 334 | parse: function(v) { 335 | return v; 336 | } 337 | }, { 338 | re: /(rgba?\([0-9.]*, ?[0-9.]*, ?[0-9.]*(?:, ?[0-9.]*)?\))/ig, 339 | klass: InterpolableColor, 340 | parse: function(v) { 341 | return v; 342 | } 343 | }, { 344 | re: /([-+]?[\d.]+)/ig, 345 | klass: InterpolableNumber, 346 | parse: parseFloat 347 | } 348 | ]; 349 | for (_i = 0, _len = types.length; _i < _len; _i++) { 350 | type = types[_i]; 351 | re = type.re; 352 | while (match = re.exec(value)) { 353 | matches.push({ 354 | index: match.index, 355 | length: match[1].length, 356 | interpolable: type.klass.create(type.parse(match[1])) 357 | }); 358 | } 359 | } 360 | matches = matches.sort(function(a, b) { 361 | return a.index > b.index; 362 | }); 363 | parts = []; 364 | index = 0; 365 | for (_j = 0, _len1 = matches.length; _j < _len1; _j++) { 366 | match = matches[_j]; 367 | if (match.index < index) { 368 | continue; 369 | } 370 | if (match.index > index) { 371 | parts.push(value.substring(index, match.index)); 372 | } 373 | parts.push(match.interpolable); 374 | index = match.index + match.length; 375 | } 376 | if (index < value.length) { 377 | parts.push(value.substring(index)); 378 | } 379 | return new InterpolableString(parts); 380 | }; 381 | 382 | return InterpolableString; 383 | 384 | })(); 385 | 386 | InterpolableObject = (function() { 387 | function InterpolableObject(obj) { 388 | this.format = __bind(this.format, this); 389 | this.interpolate = __bind(this.interpolate, this); 390 | this.obj = obj; 391 | } 392 | 393 | InterpolableObject.prototype.interpolate = function(endInterpolable, t) { 394 | var end, k, newObj, start, v; 395 | start = this.obj; 396 | end = endInterpolable.obj; 397 | newObj = {}; 398 | for (k in start) { 399 | v = start[k]; 400 | if (v.interpolate != null) { 401 | newObj[k] = v.interpolate(end[k], t); 402 | } else { 403 | newObj[k] = v; 404 | } 405 | } 406 | return new InterpolableObject(newObj); 407 | }; 408 | 409 | InterpolableObject.prototype.format = function() { 410 | return this.obj; 411 | }; 412 | 413 | InterpolableObject.create = function(value) { 414 | var k, obj, v; 415 | if (value instanceof Object) { 416 | obj = {}; 417 | for (k in value) { 418 | v = value[k]; 419 | obj[k] = createInterpolable(v); 420 | } 421 | return new InterpolableObject(obj); 422 | } 423 | return null; 424 | }; 425 | 426 | return InterpolableObject; 427 | 428 | })(); 429 | 430 | InterpolableNumber = (function() { 431 | function InterpolableNumber(value) { 432 | this.format = __bind(this.format, this); 433 | this.interpolate = __bind(this.interpolate, this); 434 | this.value = parseFloat(value); 435 | } 436 | 437 | InterpolableNumber.prototype.interpolate = function(endInterpolable, t) { 438 | var end, start; 439 | start = this.value; 440 | end = endInterpolable.value; 441 | return new InterpolableNumber((end - start) * t + start); 442 | }; 443 | 444 | InterpolableNumber.prototype.format = function() { 445 | return roundf(this.value, 5); 446 | }; 447 | 448 | InterpolableNumber.create = function(value) { 449 | if (typeof value === 'number') { 450 | return new InterpolableNumber(value); 451 | } 452 | return null; 453 | }; 454 | 455 | return InterpolableNumber; 456 | 457 | })(); 458 | 459 | InterpolableArray = (function() { 460 | function InterpolableArray(values) { 461 | this.values = values; 462 | this.format = __bind(this.format, this); 463 | this.interpolate = __bind(this.interpolate, this); 464 | } 465 | 466 | InterpolableArray.prototype.interpolate = function(endInterpolable, t) { 467 | var end, i, newValues, start, _i, _ref; 468 | start = this.values; 469 | end = endInterpolable.values; 470 | newValues = []; 471 | for (i = _i = 0, _ref = Math.min(start.length, end.length); 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 472 | if (start[i].interpolate != null) { 473 | newValues.push(start[i].interpolate(end[i], t)); 474 | } else { 475 | newValues.push(start[i]); 476 | } 477 | } 478 | return new InterpolableArray(newValues); 479 | }; 480 | 481 | InterpolableArray.prototype.format = function() { 482 | return this.values.map(function(val) { 483 | if (val.format != null) { 484 | return val.format(); 485 | } else { 486 | return val; 487 | } 488 | }); 489 | }; 490 | 491 | InterpolableArray.createFromArray = function(arr) { 492 | var values; 493 | values = arr.map(function(val) { 494 | return createInterpolable(val) || val; 495 | }); 496 | values = values.filter(function(val) { 497 | return val != null; 498 | }); 499 | return new InterpolableArray(values); 500 | }; 501 | 502 | InterpolableArray.create = function(value) { 503 | if (value instanceof Array) { 504 | return InterpolableArray.createFromArray(value); 505 | } 506 | return null; 507 | }; 508 | 509 | return InterpolableArray; 510 | 511 | })(); 512 | 513 | Color = (function() { 514 | function Color(rgb, format) { 515 | this.rgb = rgb != null ? rgb : {}; 516 | this.format = format; 517 | this.toRgba = __bind(this.toRgba, this); 518 | this.toRgb = __bind(this.toRgb, this); 519 | this.toHex = __bind(this.toHex, this); 520 | } 521 | 522 | Color.fromHex = function(hex) { 523 | var hex3, result; 524 | hex3 = hex.match(/^#([a-f\d]{1})([a-f\d]{1})([a-f\d]{1})$/i); 525 | if (hex3 != null) { 526 | hex = "#" + hex3[1] + hex3[1] + hex3[2] + hex3[2] + hex3[3] + hex3[3]; 527 | } 528 | result = hex.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i); 529 | if (result != null) { 530 | return new Color({ 531 | r: parseInt(result[1], 16), 532 | g: parseInt(result[2], 16), 533 | b: parseInt(result[3], 16), 534 | a: 1 535 | }, "hex"); 536 | } 537 | return null; 538 | }; 539 | 540 | Color.fromRgb = function(rgb) { 541 | var match, _ref; 542 | match = rgb.match(/^rgba?\(([0-9.]*), ?([0-9.]*), ?([0-9.]*)(?:, ?([0-9.]*))?\)$/); 543 | if (match != null) { 544 | return new Color({ 545 | r: parseFloat(match[1]), 546 | g: parseFloat(match[2]), 547 | b: parseFloat(match[3]), 548 | a: parseFloat((_ref = match[4]) != null ? _ref : 1) 549 | }, match[4] != null ? "rgba" : "rgb"); 550 | } 551 | return null; 552 | }; 553 | 554 | Color.componentToHex = function(c) { 555 | var hex; 556 | hex = c.toString(16); 557 | if (hex.length === 1) { 558 | return "0" + hex; 559 | } else { 560 | return hex; 561 | } 562 | }; 563 | 564 | Color.prototype.toHex = function() { 565 | return "#" + Color.componentToHex(this.rgb.r) + Color.componentToHex(this.rgb.g) + Color.componentToHex(this.rgb.b); 566 | }; 567 | 568 | Color.prototype.toRgb = function() { 569 | return "rgb(" + this.rgb.r + ", " + this.rgb.g + ", " + this.rgb.b + ")"; 570 | }; 571 | 572 | Color.prototype.toRgba = function() { 573 | return "rgba(" + this.rgb.r + ", " + this.rgb.g + ", " + this.rgb.b + ", " + this.rgb.a + ")"; 574 | }; 575 | 576 | return Color; 577 | 578 | })(); 579 | 580 | InterpolableColor = (function() { 581 | function InterpolableColor(color) { 582 | this.color = color; 583 | this.format = __bind(this.format, this); 584 | this.interpolate = __bind(this.interpolate, this); 585 | } 586 | 587 | InterpolableColor.prototype.interpolate = function(endInterpolable, t) { 588 | var end, k, rgb, start, v, _i, _len, _ref; 589 | start = this.color; 590 | end = endInterpolable.color; 591 | rgb = {}; 592 | _ref = ['r', 'g', 'b']; 593 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 594 | k = _ref[_i]; 595 | v = Math.round((end.rgb[k] - start.rgb[k]) * t + start.rgb[k]); 596 | rgb[k] = Math.min(255, Math.max(0, v)); 597 | } 598 | k = "a"; 599 | v = roundf((end.rgb[k] - start.rgb[k]) * t + start.rgb[k], 5); 600 | rgb[k] = Math.min(1, Math.max(0, v)); 601 | return new InterpolableColor(new Color(rgb, end.format)); 602 | }; 603 | 604 | InterpolableColor.prototype.format = function() { 605 | if (this.color.format === "hex") { 606 | return this.color.toHex(); 607 | } else if (this.color.format === "rgb") { 608 | return this.color.toRgb(); 609 | } else if (this.color.format === "rgba") { 610 | return this.color.toRgba(); 611 | } 612 | }; 613 | 614 | InterpolableColor.create = function(value) { 615 | var color; 616 | if (typeof value !== "string") { 617 | return; 618 | } 619 | color = Color.fromHex(value) || Color.fromRgb(value); 620 | if (color != null) { 621 | return new InterpolableColor(color); 622 | } 623 | return null; 624 | }; 625 | 626 | return InterpolableColor; 627 | 628 | })(); 629 | 630 | DecomposedMatrix2D = (function() { 631 | function DecomposedMatrix2D(props) { 632 | this.props = props; 633 | this.applyRotateCenter = __bind(this.applyRotateCenter, this); 634 | this.format = __bind(this.format, this); 635 | this.interpolate = __bind(this.interpolate, this); 636 | } 637 | 638 | DecomposedMatrix2D.prototype.interpolate = function(endMatrix, t) { 639 | var i, k, newProps, _i, _j, _k, _l, _len, _len1, _ref, _ref1, _ref2; 640 | newProps = {}; 641 | _ref = ['translate', 'scale', 'rotate']; 642 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 643 | k = _ref[_i]; 644 | newProps[k] = []; 645 | for (i = _j = 0, _ref1 = this.props[k].length; 0 <= _ref1 ? _j < _ref1 : _j > _ref1; i = 0 <= _ref1 ? ++_j : --_j) { 646 | newProps[k][i] = (endMatrix.props[k][i] - this.props[k][i]) * t + this.props[k][i]; 647 | } 648 | } 649 | for (i = _k = 1; _k <= 2; i = ++_k) { 650 | newProps['rotate'][i] = endMatrix.props['rotate'][i]; 651 | } 652 | _ref2 = ['skew']; 653 | for (_l = 0, _len1 = _ref2.length; _l < _len1; _l++) { 654 | k = _ref2[_l]; 655 | newProps[k] = (endMatrix.props[k] - this.props[k]) * t + this.props[k]; 656 | } 657 | return new DecomposedMatrix2D(newProps); 658 | }; 659 | 660 | DecomposedMatrix2D.prototype.format = function() { 661 | return "translate(" + (this.props.translate.join(',')) + ") rotate(" + (this.props.rotate.join(',')) + ") skewX(" + this.props.skew + ") scale(" + (this.props.scale.join(',')) + ")"; 662 | }; 663 | 664 | DecomposedMatrix2D.prototype.applyRotateCenter = function(rotateC) { 665 | var i, m, m2d, negativeTranslate, _i, _results; 666 | m = baseSVG.createSVGMatrix(); 667 | m = m.translate(rotateC[0], rotateC[1]); 668 | m = m.rotate(this.props.rotate[0]); 669 | m = m.translate(-rotateC[0], -rotateC[1]); 670 | m2d = new Matrix2D(m); 671 | negativeTranslate = m2d.decompose().props.translate; 672 | _results = []; 673 | for (i = _i = 0; _i <= 1; i = ++_i) { 674 | _results.push(this.props.translate[i] -= negativeTranslate[i]); 675 | } 676 | return _results; 677 | }; 678 | 679 | return DecomposedMatrix2D; 680 | 681 | })(); 682 | 683 | baseSVG = typeof document !== "undefined" && document !== null ? document.createElementNS("http://www.w3.org/2000/svg", "svg") : void 0; 684 | 685 | Matrix2D = (function() { 686 | function Matrix2D(m) { 687 | this.m = m; 688 | this.applyProperties = __bind(this.applyProperties, this); 689 | this.decompose = __bind(this.decompose, this); 690 | if (!this.m) { 691 | this.m = baseSVG.createSVGMatrix(); 692 | } 693 | } 694 | 695 | Matrix2D.prototype.decompose = function() { 696 | var kx, ky, kz, r0, r1; 697 | r0 = new Vector([this.m.a, this.m.b]); 698 | r1 = new Vector([this.m.c, this.m.d]); 699 | kx = r0.length(); 700 | kz = r0.dot(r1); 701 | r0 = r0.normalize(); 702 | ky = r1.combine(r0, 1, -kz).length(); 703 | return new DecomposedMatrix2D({ 704 | translate: [this.m.e, this.m.f], 705 | rotate: [Math.atan2(this.m.b, this.m.a) * 180 / Math.PI, this.rotateCX, this.rotateCY], 706 | scale: [kx, ky], 707 | skew: kz / ky * 180 / Math.PI 708 | }); 709 | }; 710 | 711 | Matrix2D.prototype.applyProperties = function(properties) { 712 | var hash, k, props, v, _i, _len, _ref, _ref1; 713 | hash = {}; 714 | for (_i = 0, _len = properties.length; _i < _len; _i++) { 715 | props = properties[_i]; 716 | hash[props[0]] = props[1]; 717 | } 718 | for (k in hash) { 719 | v = hash[k]; 720 | if (k === "translateX") { 721 | this.m = this.m.translate(v, 0); 722 | } else if (k === "translateY") { 723 | this.m = this.m.translate(0, v); 724 | } else if (k === "scaleX") { 725 | this.m = this.m.scale(v, 1); 726 | } else if (k === "scaleY") { 727 | this.m = this.m.scale(1, v); 728 | } else if (k === "rotateZ") { 729 | this.m = this.m.rotate(v); 730 | } else if (k === "skewX") { 731 | this.m = this.m.skewX(v); 732 | } else if (k === "skewY") { 733 | this.m = this.m.skewY(v); 734 | } 735 | } 736 | this.rotateCX = (_ref = hash.rotateCX) != null ? _ref : 0; 737 | return this.rotateCY = (_ref1 = hash.rotateCY) != null ? _ref1 : 0; 738 | }; 739 | 740 | return Matrix2D; 741 | 742 | })(); 743 | 744 | Vector = (function() { 745 | function Vector(els) { 746 | this.els = els; 747 | this.combine = __bind(this.combine, this); 748 | this.normalize = __bind(this.normalize, this); 749 | this.length = __bind(this.length, this); 750 | this.cross = __bind(this.cross, this); 751 | this.dot = __bind(this.dot, this); 752 | this.e = __bind(this.e, this); 753 | } 754 | 755 | Vector.prototype.e = function(i) { 756 | if (i < 1 || i > this.els.length) { 757 | return null; 758 | } else { 759 | return this.els[i - 1]; 760 | } 761 | }; 762 | 763 | Vector.prototype.dot = function(vector) { 764 | var V, n, product; 765 | V = vector.els || vector; 766 | product = 0; 767 | n = this.els.length; 768 | if (n !== V.length) { 769 | return null; 770 | } 771 | n += 1; 772 | while (--n) { 773 | product += this.els[n - 1] * V[n - 1]; 774 | } 775 | return product; 776 | }; 777 | 778 | Vector.prototype.cross = function(vector) { 779 | var A, B; 780 | B = vector.els || vector; 781 | if (this.els.length !== 3 || B.length !== 3) { 782 | return null; 783 | } 784 | A = this.els; 785 | return new Vector([(A[1] * B[2]) - (A[2] * B[1]), (A[2] * B[0]) - (A[0] * B[2]), (A[0] * B[1]) - (A[1] * B[0])]); 786 | }; 787 | 788 | Vector.prototype.length = function() { 789 | var a, e, _i, _len, _ref; 790 | a = 0; 791 | _ref = this.els; 792 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 793 | e = _ref[_i]; 794 | a += Math.pow(e, 2); 795 | } 796 | return Math.sqrt(a); 797 | }; 798 | 799 | Vector.prototype.normalize = function() { 800 | var e, i, length, newElements, _ref; 801 | length = this.length(); 802 | newElements = []; 803 | _ref = this.els; 804 | for (i in _ref) { 805 | e = _ref[i]; 806 | newElements[i] = e / length; 807 | } 808 | return new Vector(newElements); 809 | }; 810 | 811 | Vector.prototype.combine = function(b, ascl, bscl) { 812 | var i, result, _i, _ref; 813 | result = []; 814 | for (i = _i = 0, _ref = this.els.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 815 | result[i] = (ascl * this.els[i]) + (bscl * b.els[i]); 816 | } 817 | return new Vector(result); 818 | }; 819 | 820 | return Vector; 821 | 822 | })(); 823 | 824 | DecomposedMatrix = (function() { 825 | function DecomposedMatrix() { 826 | this.valueForKey = __bind(this.valueForKey, this); 827 | this.toMatrix = __bind(this.toMatrix, this); 828 | this.format = __bind(this.format, this); 829 | this.interpolate = __bind(this.interpolate, this); 830 | } 831 | 832 | DecomposedMatrix.prototype.interpolate = function(decomposedB, t, only) { 833 | var angle, decomposed, decomposedA, i, invscale, invth, k, qa, qb, scale, th, _i, _j, _k, _l, _len, _ref, _ref1; 834 | if (only == null) { 835 | only = null; 836 | } 837 | decomposedA = this; 838 | decomposed = new DecomposedMatrix; 839 | _ref = ['translate', 'scale', 'skew', 'perspective']; 840 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 841 | k = _ref[_i]; 842 | decomposed[k] = []; 843 | for (i = _j = 0, _ref1 = decomposedA[k].length - 1; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) { 844 | if ((only == null) || only.indexOf(k) > -1 || only.indexOf("" + k + ['x', 'y', 'z'][i]) > -1) { 845 | decomposed[k][i] = (decomposedB[k][i] - decomposedA[k][i]) * t + decomposedA[k][i]; 846 | } else { 847 | decomposed[k][i] = decomposedA[k][i]; 848 | } 849 | } 850 | } 851 | if ((only == null) || only.indexOf('rotate') !== -1) { 852 | qa = decomposedA.quaternion; 853 | qb = decomposedB.quaternion; 854 | angle = qa[0] * qb[0] + qa[1] * qb[1] + qa[2] * qb[2] + qa[3] * qb[3]; 855 | if (angle < 0.0) { 856 | for (i = _k = 0; _k <= 3; i = ++_k) { 857 | qa[i] = -qa[i]; 858 | } 859 | angle = -angle; 860 | } 861 | if (angle + 1.0 > .05) { 862 | if (1.0 - angle >= .05) { 863 | th = Math.acos(angle); 864 | invth = 1.0 / Math.sin(th); 865 | scale = Math.sin(th * (1.0 - t)) * invth; 866 | invscale = Math.sin(th * t) * invth; 867 | } else { 868 | scale = 1.0 - t; 869 | invscale = t; 870 | } 871 | } else { 872 | qb[0] = -qa[1]; 873 | qb[1] = qa[0]; 874 | qb[2] = -qa[3]; 875 | qb[3] = qa[2]; 876 | scale = Math.sin(piDouble * (.5 - t)); 877 | invscale = Math.sin(piDouble * t); 878 | } 879 | decomposed.quaternion = []; 880 | for (i = _l = 0; _l <= 3; i = ++_l) { 881 | decomposed.quaternion[i] = qa[i] * scale + qb[i] * invscale; 882 | } 883 | } else { 884 | decomposed.quaternion = decomposedA.quaternion; 885 | } 886 | return decomposed; 887 | }; 888 | 889 | DecomposedMatrix.prototype.format = function() { 890 | return this.toMatrix().toString(); 891 | }; 892 | 893 | DecomposedMatrix.prototype.toMatrix = function() { 894 | var decomposedMatrix, i, j, match, matrix, quaternion, skew, temp, w, x, y, z, _i, _j, _k, _l; 895 | decomposedMatrix = this; 896 | matrix = Matrix.I(4); 897 | for (i = _i = 0; _i <= 3; i = ++_i) { 898 | matrix.els[i][3] = decomposedMatrix.perspective[i]; 899 | } 900 | quaternion = decomposedMatrix.quaternion; 901 | x = quaternion[0]; 902 | y = quaternion[1]; 903 | z = quaternion[2]; 904 | w = quaternion[3]; 905 | skew = decomposedMatrix.skew; 906 | match = [[1, 0], [2, 0], [2, 1]]; 907 | for (i = _j = 2; _j >= 0; i = --_j) { 908 | if (skew[i]) { 909 | temp = Matrix.I(4); 910 | temp.els[match[i][0]][match[i][1]] = skew[i]; 911 | matrix = matrix.multiply(temp); 912 | } 913 | } 914 | matrix = matrix.multiply(new Matrix([[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w), 0], [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w), 0], [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y), 0], [0, 0, 0, 1]])); 915 | for (i = _k = 0; _k <= 2; i = ++_k) { 916 | for (j = _l = 0; _l <= 2; j = ++_l) { 917 | matrix.els[i][j] *= decomposedMatrix.scale[i]; 918 | } 919 | matrix.els[3][i] = decomposedMatrix.translate[i]; 920 | } 921 | return matrix; 922 | }; 923 | 924 | DecomposedMatrix.prototype.valueForKey = function(key) { 925 | var axis, axiss, index, validKey, validKeys, _i, _len; 926 | validKeys = ['transform', 'scale', 'rotate']; 927 | axiss = ['x', 'y', 'z']; 928 | axis = null; 929 | for (_i = 0, _len = validKeys.length; _i < _len; _i++) { 930 | validKey = validKeys[_i]; 931 | if (key.match(validKey)) { 932 | axis = validKey.toLowerCase().substring(validKeys.length - 1, 1); 933 | index = axiss.indexOf(axis); 934 | if (index > -1) { 935 | return this[key][index]; 936 | } 937 | break; 938 | } 939 | } 940 | return 0; 941 | }; 942 | 943 | return DecomposedMatrix; 944 | 945 | })(); 946 | 947 | Matrix = (function() { 948 | function Matrix(els) { 949 | this.els = els; 950 | this.toString = __bind(this.toString, this); 951 | this.decompose = __bind(this.decompose, this); 952 | this.inverse = __bind(this.inverse, this); 953 | this.augment = __bind(this.augment, this); 954 | this.toRightTriangular = __bind(this.toRightTriangular, this); 955 | this.transpose = __bind(this.transpose, this); 956 | this.multiply = __bind(this.multiply, this); 957 | this.dup = __bind(this.dup, this); 958 | this.e = __bind(this.e, this); 959 | } 960 | 961 | Matrix.prototype.e = function(i, j) { 962 | if (i < 1 || i > this.els.length || j < 1 || j > this.els[0].length) { 963 | return null; 964 | } 965 | return this.els[i - 1][j - 1]; 966 | }; 967 | 968 | Matrix.prototype.dup = function() { 969 | return new Matrix(this.els); 970 | }; 971 | 972 | Matrix.prototype.multiply = function(matrix) { 973 | var M, c, cols, elements, i, j, ki, kj, nc, ni, nj, returnVector, sum; 974 | returnVector = matrix.modulus ? true : false; 975 | M = matrix.els || matrix; 976 | if (typeof M[0][0] === 'undefined') { 977 | M = new Matrix(M).els; 978 | } 979 | ni = this.els.length; 980 | ki = ni; 981 | kj = M[0].length; 982 | cols = this.els[0].length; 983 | elements = []; 984 | ni += 1; 985 | while (--ni) { 986 | i = ki - ni; 987 | elements[i] = []; 988 | nj = kj; 989 | nj += 1; 990 | while (--nj) { 991 | j = kj - nj; 992 | sum = 0; 993 | nc = cols; 994 | nc += 1; 995 | while (--nc) { 996 | c = cols - nc; 997 | sum += this.els[i][c] * M[c][j]; 998 | } 999 | elements[i][j] = sum; 1000 | } 1001 | } 1002 | M = new Matrix(elements); 1003 | if (returnVector) { 1004 | return M.col(1); 1005 | } else { 1006 | return M; 1007 | } 1008 | }; 1009 | 1010 | Matrix.prototype.transpose = function() { 1011 | var cols, elements, i, j, ni, nj, rows; 1012 | rows = this.els.length; 1013 | cols = this.els[0].length; 1014 | elements = []; 1015 | ni = cols; 1016 | ni += 1; 1017 | while (--ni) { 1018 | i = cols - ni; 1019 | elements[i] = []; 1020 | nj = rows; 1021 | nj += 1; 1022 | while (--nj) { 1023 | j = rows - nj; 1024 | elements[i][j] = this.els[j][i]; 1025 | } 1026 | } 1027 | return new Matrix(elements); 1028 | }; 1029 | 1030 | Matrix.prototype.toRightTriangular = function() { 1031 | var M, els, i, j, k, kp, multiplier, n, np, p, _i, _j, _ref, _ref1; 1032 | M = this.dup(); 1033 | n = this.els.length; 1034 | k = n; 1035 | kp = this.els[0].length; 1036 | while (--n) { 1037 | i = k - n; 1038 | if (M.els[i][i] === 0) { 1039 | for (j = _i = _ref = i + 1; _ref <= k ? _i < k : _i > k; j = _ref <= k ? ++_i : --_i) { 1040 | if (M.els[j][i] !== 0) { 1041 | els = []; 1042 | np = kp; 1043 | np += 1; 1044 | while (--np) { 1045 | p = kp - np; 1046 | els.push(M.els[i][p] + M.els[j][p]); 1047 | } 1048 | M.els[i] = els; 1049 | break; 1050 | } 1051 | } 1052 | } 1053 | if (M.els[i][i] !== 0) { 1054 | for (j = _j = _ref1 = i + 1; _ref1 <= k ? _j < k : _j > k; j = _ref1 <= k ? ++_j : --_j) { 1055 | multiplier = M.els[j][i] / M.els[i][i]; 1056 | els = []; 1057 | np = kp; 1058 | np += 1; 1059 | while (--np) { 1060 | p = kp - np; 1061 | els.push(p <= i ? 0 : M.els[j][p] - M.els[i][p] * multiplier); 1062 | } 1063 | M.els[j] = els; 1064 | } 1065 | } 1066 | } 1067 | return M; 1068 | }; 1069 | 1070 | Matrix.prototype.augment = function(matrix) { 1071 | var M, T, cols, i, j, ki, kj, ni, nj; 1072 | M = matrix.els || matrix; 1073 | if (typeof M[0][0] === 'undefined') { 1074 | M = new Matrix(M).els; 1075 | } 1076 | T = this.dup(); 1077 | cols = T.els[0].length; 1078 | ni = T.els.length; 1079 | ki = ni; 1080 | kj = M[0].length; 1081 | if (ni !== M.length) { 1082 | return null; 1083 | } 1084 | ni += 1; 1085 | while (--ni) { 1086 | i = ki - ni; 1087 | nj = kj; 1088 | nj += 1; 1089 | while (--nj) { 1090 | j = kj - nj; 1091 | T.els[i][cols + j] = M[i][j]; 1092 | } 1093 | } 1094 | return T; 1095 | }; 1096 | 1097 | Matrix.prototype.inverse = function() { 1098 | var M, divisor, els, i, inverse_elements, j, ki, kp, new_element, ni, np, p, _i; 1099 | ni = this.els.length; 1100 | ki = ni; 1101 | M = this.augment(Matrix.I(ni)).toRightTriangular(); 1102 | kp = M.els[0].length; 1103 | inverse_elements = []; 1104 | ni += 1; 1105 | while (--ni) { 1106 | i = ni - 1; 1107 | els = []; 1108 | np = kp; 1109 | inverse_elements[i] = []; 1110 | divisor = M.els[i][i]; 1111 | np += 1; 1112 | while (--np) { 1113 | p = kp - np; 1114 | new_element = M.els[i][p] / divisor; 1115 | els.push(new_element); 1116 | if (p >= ki) { 1117 | inverse_elements[i].push(new_element); 1118 | } 1119 | } 1120 | M.els[i] = els; 1121 | for (j = _i = 0; 0 <= i ? _i < i : _i > i; j = 0 <= i ? ++_i : --_i) { 1122 | els = []; 1123 | np = kp; 1124 | np += 1; 1125 | while (--np) { 1126 | p = kp - np; 1127 | els.push(M.els[j][p] - M.els[i][p] * M.els[j][i]); 1128 | } 1129 | M.els[j] = els; 1130 | } 1131 | } 1132 | return new Matrix(inverse_elements); 1133 | }; 1134 | 1135 | Matrix.I = function(n) { 1136 | var els, i, j, k, nj; 1137 | els = []; 1138 | k = n; 1139 | n += 1; 1140 | while (--n) { 1141 | i = k - n; 1142 | els[i] = []; 1143 | nj = k; 1144 | nj += 1; 1145 | while (--nj) { 1146 | j = k - nj; 1147 | els[i][j] = i === j ? 1 : 0; 1148 | } 1149 | } 1150 | return new Matrix(els); 1151 | }; 1152 | 1153 | Matrix.prototype.decompose = function() { 1154 | var els, i, inversePerspectiveMatrix, j, k, matrix, pdum3, perspective, perspectiveMatrix, quaternion, result, rightHandSide, rotate, row, rowElement, s, scale, skew, t, translate, transposedInversePerspectiveMatrix, type, typeKey, v, w, x, y, z, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r; 1155 | matrix = this; 1156 | translate = []; 1157 | scale = []; 1158 | skew = []; 1159 | quaternion = []; 1160 | perspective = []; 1161 | els = []; 1162 | for (i = _i = 0; _i <= 3; i = ++_i) { 1163 | els[i] = []; 1164 | for (j = _j = 0; _j <= 3; j = ++_j) { 1165 | els[i][j] = matrix.els[i][j]; 1166 | } 1167 | } 1168 | if (els[3][3] === 0) { 1169 | return false; 1170 | } 1171 | for (i = _k = 0; _k <= 3; i = ++_k) { 1172 | for (j = _l = 0; _l <= 3; j = ++_l) { 1173 | els[i][j] /= els[3][3]; 1174 | } 1175 | } 1176 | perspectiveMatrix = matrix.dup(); 1177 | for (i = _m = 0; _m <= 2; i = ++_m) { 1178 | perspectiveMatrix.els[i][3] = 0; 1179 | } 1180 | perspectiveMatrix.els[3][3] = 1; 1181 | if (els[0][3] !== 0 || els[1][3] !== 0 || els[2][3] !== 0) { 1182 | rightHandSide = new Vector(els.slice(0, 4)[3]); 1183 | inversePerspectiveMatrix = perspectiveMatrix.inverse(); 1184 | transposedInversePerspectiveMatrix = inversePerspectiveMatrix.transpose(); 1185 | perspective = transposedInversePerspectiveMatrix.multiply(rightHandSide).els; 1186 | for (i = _n = 0; _n <= 2; i = ++_n) { 1187 | els[i][3] = 0; 1188 | } 1189 | els[3][3] = 1; 1190 | } else { 1191 | perspective = [0, 0, 0, 1]; 1192 | } 1193 | for (i = _o = 0; _o <= 2; i = ++_o) { 1194 | translate[i] = els[3][i]; 1195 | els[3][i] = 0; 1196 | } 1197 | row = []; 1198 | for (i = _p = 0; _p <= 2; i = ++_p) { 1199 | row[i] = new Vector(els[i].slice(0, 3)); 1200 | } 1201 | scale[0] = row[0].length(); 1202 | row[0] = row[0].normalize(); 1203 | skew[0] = row[0].dot(row[1]); 1204 | row[1] = row[1].combine(row[0], 1.0, -skew[0]); 1205 | scale[1] = row[1].length(); 1206 | row[1] = row[1].normalize(); 1207 | skew[0] /= scale[1]; 1208 | skew[1] = row[0].dot(row[2]); 1209 | row[2] = row[2].combine(row[0], 1.0, -skew[1]); 1210 | skew[2] = row[1].dot(row[2]); 1211 | row[2] = row[2].combine(row[1], 1.0, -skew[2]); 1212 | scale[2] = row[2].length(); 1213 | row[2] = row[2].normalize(); 1214 | skew[1] /= scale[2]; 1215 | skew[2] /= scale[2]; 1216 | pdum3 = row[1].cross(row[2]); 1217 | if (row[0].dot(pdum3) < 0) { 1218 | for (i = _q = 0; _q <= 2; i = ++_q) { 1219 | scale[i] *= -1; 1220 | for (j = _r = 0; _r <= 2; j = ++_r) { 1221 | row[i].els[j] *= -1; 1222 | } 1223 | } 1224 | } 1225 | rowElement = function(index, elementIndex) { 1226 | return row[index].els[elementIndex]; 1227 | }; 1228 | rotate = []; 1229 | rotate[1] = Math.asin(-rowElement(0, 2)); 1230 | if (Math.cos(rotate[1]) !== 0) { 1231 | rotate[0] = Math.atan2(rowElement(1, 2), rowElement(2, 2)); 1232 | rotate[2] = Math.atan2(rowElement(0, 1), rowElement(0, 0)); 1233 | } else { 1234 | rotate[0] = Math.atan2(-rowElement(2, 0), rowElement(1, 1)); 1235 | rotate[1] = 0; 1236 | } 1237 | t = rowElement(0, 0) + rowElement(1, 1) + rowElement(2, 2) + 1.0; 1238 | if (t > 1e-4) { 1239 | s = 0.5 / Math.sqrt(t); 1240 | w = 0.25 / s; 1241 | x = (rowElement(2, 1) - rowElement(1, 2)) * s; 1242 | y = (rowElement(0, 2) - rowElement(2, 0)) * s; 1243 | z = (rowElement(1, 0) - rowElement(0, 1)) * s; 1244 | } else if ((rowElement(0, 0) > rowElement(1, 1)) && (rowElement(0, 0) > rowElement(2, 2))) { 1245 | s = Math.sqrt(1.0 + rowElement(0, 0) - rowElement(1, 1) - rowElement(2, 2)) * 2.0; 1246 | x = 0.25 * s; 1247 | y = (rowElement(0, 1) + rowElement(1, 0)) / s; 1248 | z = (rowElement(0, 2) + rowElement(2, 0)) / s; 1249 | w = (rowElement(2, 1) - rowElement(1, 2)) / s; 1250 | } else if (rowElement(1, 1) > rowElement(2, 2)) { 1251 | s = Math.sqrt(1.0 + rowElement(1, 1) - rowElement(0, 0) - rowElement(2, 2)) * 2.0; 1252 | x = (rowElement(0, 1) + rowElement(1, 0)) / s; 1253 | y = 0.25 * s; 1254 | z = (rowElement(1, 2) + rowElement(2, 1)) / s; 1255 | w = (rowElement(0, 2) - rowElement(2, 0)) / s; 1256 | } else { 1257 | s = Math.sqrt(1.0 + rowElement(2, 2) - rowElement(0, 0) - rowElement(1, 1)) * 2.0; 1258 | x = (rowElement(0, 2) + rowElement(2, 0)) / s; 1259 | y = (rowElement(1, 2) + rowElement(2, 1)) / s; 1260 | z = 0.25 * s; 1261 | w = (rowElement(1, 0) - rowElement(0, 1)) / s; 1262 | } 1263 | quaternion = [x, y, z, w]; 1264 | result = new DecomposedMatrix; 1265 | result.translate = translate; 1266 | result.scale = scale; 1267 | result.skew = skew; 1268 | result.quaternion = quaternion; 1269 | result.perspective = perspective; 1270 | result.rotate = rotate; 1271 | for (typeKey in result) { 1272 | type = result[typeKey]; 1273 | for (k in type) { 1274 | v = type[k]; 1275 | if (isNaN(v)) { 1276 | type[k] = 0; 1277 | } 1278 | } 1279 | } 1280 | return result; 1281 | }; 1282 | 1283 | Matrix.prototype.toString = function() { 1284 | var i, j, str, _i, _j; 1285 | str = 'matrix3d('; 1286 | for (i = _i = 0; _i <= 3; i = ++_i) { 1287 | for (j = _j = 0; _j <= 3; j = ++_j) { 1288 | str += roundf(this.els[i][j], 10); 1289 | if (!(i === 3 && j === 3)) { 1290 | str += ','; 1291 | } 1292 | } 1293 | } 1294 | str += ')'; 1295 | return str; 1296 | }; 1297 | 1298 | Matrix.matrixForTransform = cacheFn(function(transform) { 1299 | var matrixEl, result, style, _ref, _ref1, _ref2; 1300 | matrixEl = document.createElement('div'); 1301 | matrixEl.style.position = 'absolute'; 1302 | matrixEl.style.visibility = 'hidden'; 1303 | matrixEl.style[propertyWithPrefix("transform")] = transform; 1304 | document.body.appendChild(matrixEl); 1305 | style = window.getComputedStyle(matrixEl, null); 1306 | result = (_ref = (_ref1 = style.transform) != null ? _ref1 : style[propertyWithPrefix("transform")]) != null ? _ref : (_ref2 = dynamics.tests) != null ? _ref2.matrixForTransform(transform) : void 0; 1307 | document.body.removeChild(matrixEl); 1308 | return result; 1309 | }); 1310 | 1311 | Matrix.fromTransform = function(transform) { 1312 | var digits, elements, i, match, matrixElements, _i; 1313 | match = transform != null ? transform.match(/matrix3?d?\(([-0-9,e \.]*)\)/) : void 0; 1314 | if (match) { 1315 | digits = match[1].split(','); 1316 | digits = digits.map(parseFloat); 1317 | if (digits.length === 6) { 1318 | elements = [digits[0], digits[1], 0, 0, digits[2], digits[3], 0, 0, 0, 0, 1, 0, digits[4], digits[5], 0, 1]; 1319 | } else { 1320 | elements = digits; 1321 | } 1322 | } else { 1323 | elements = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; 1324 | } 1325 | matrixElements = []; 1326 | for (i = _i = 0; _i <= 3; i = ++_i) { 1327 | matrixElements.push(elements.slice(i * 4, i * 4 + 4)); 1328 | } 1329 | return new Matrix(matrixElements); 1330 | }; 1331 | 1332 | return Matrix; 1333 | 1334 | })(); 1335 | 1336 | prefixFor = cacheFn(function(property) { 1337 | var k, prefix, prop, propArray, propertyName, _i, _j, _len, _len1, _ref; 1338 | if (document.body.style[property] !== void 0) { 1339 | return ''; 1340 | } 1341 | propArray = property.split('-'); 1342 | propertyName = ""; 1343 | for (_i = 0, _len = propArray.length; _i < _len; _i++) { 1344 | prop = propArray[_i]; 1345 | propertyName += prop.substring(0, 1).toUpperCase() + prop.substring(1); 1346 | } 1347 | _ref = ["Webkit", "Moz", "ms"]; 1348 | for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { 1349 | prefix = _ref[_j]; 1350 | k = prefix + propertyName; 1351 | if (document.body.style[k] !== void 0) { 1352 | return prefix; 1353 | } 1354 | } 1355 | return ''; 1356 | }); 1357 | 1358 | propertyWithPrefix = cacheFn(function(property) { 1359 | var prefix; 1360 | prefix = prefixFor(property); 1361 | if (prefix === 'Moz') { 1362 | return "" + prefix + (property.substring(0, 1).toUpperCase() + property.substring(1)); 1363 | } 1364 | if (prefix !== '') { 1365 | return "-" + (prefix.toLowerCase()) + "-" + (toDashed(property)); 1366 | } 1367 | return toDashed(property); 1368 | }); 1369 | 1370 | rAF = typeof window !== "undefined" && window !== null ? window.requestAnimationFrame : void 0; 1371 | 1372 | animations = []; 1373 | 1374 | animationsTimeouts = []; 1375 | 1376 | runloops = []; 1377 | 1378 | slow = false; 1379 | 1380 | slowRatio = 1; 1381 | 1382 | if (typeof window !== "undefined" && window !== null) { 1383 | window.addEventListener('keyup', function(e) { 1384 | if (e.keyCode === 68 && e.shiftKey && e.ctrlKey) { 1385 | return dynamics.toggleSlow(); 1386 | } 1387 | }); 1388 | } 1389 | 1390 | if (rAF == null) { 1391 | lastTime = 0; 1392 | rAF = function(callback) { 1393 | var currTime, id, timeToCall; 1394 | currTime = Date.now(); 1395 | timeToCall = Math.max(0, 16 - (currTime - lastTime)); 1396 | id = window.setTimeout(function() { 1397 | return callback(currTime + timeToCall); 1398 | }, timeToCall); 1399 | lastTime = currTime + timeToCall; 1400 | return id; 1401 | }; 1402 | } 1403 | 1404 | runLoopRunning = false; 1405 | 1406 | runLoopPaused = false; 1407 | 1408 | startMainRunLoop = function() { 1409 | if (!runLoopRunning) { 1410 | runLoopRunning = true; 1411 | return rAF(tick); 1412 | } 1413 | }; 1414 | 1415 | tick = function(t) { 1416 | var animation, runloop, toRemoveAnimations, _i, _j, _len, _len1; 1417 | if (runLoopPaused) { 1418 | rAF(tick); 1419 | return; 1420 | } 1421 | for (_i = 0, _len = runloops.length; _i < _len; _i++) { 1422 | runloop = runloops[_i]; 1423 | runloopTick(t, runloop); 1424 | } 1425 | toRemoveAnimations = []; 1426 | for (_j = 0, _len1 = animations.length; _j < _len1; _j++) { 1427 | animation = animations[_j]; 1428 | if (!animationTick(t, animation)) { 1429 | toRemoveAnimations.push(animation); 1430 | } 1431 | } 1432 | animations = animations.filter(function(animation) { 1433 | return toRemoveAnimations.indexOf(animation) === -1; 1434 | }); 1435 | if (animations.length === 0 && runloops.length === 0) { 1436 | return runLoopRunning = false; 1437 | } else { 1438 | return rAF(tick); 1439 | } 1440 | }; 1441 | 1442 | runloopTick = function(t, runloop) { 1443 | var dt, key, properties, property, _ref; 1444 | t = t / 1000; 1445 | if (runloop.tStart == null) { 1446 | runloop.tStart = t; 1447 | } 1448 | if (runloop.previousT == null) { 1449 | runloop.previousT = t; 1450 | } 1451 | dt = t - runloop.previousT; 1452 | runloop.previousT = t; 1453 | properties = {}; 1454 | _ref = runloop.properties.current; 1455 | for (key in _ref) { 1456 | property = _ref[key]; 1457 | properties[key] = runloop.properties.end[key](property, t, dt); 1458 | } 1459 | applyFrame(runloop.el, properties); 1460 | runloop.properties.current = properties; 1461 | return true; 1462 | }; 1463 | 1464 | animationTick = function(t, animation) { 1465 | var key, properties, property, tt, y, _base, _base1, _ref; 1466 | if (animation.tStart == null) { 1467 | animation.tStart = t; 1468 | } 1469 | tt = (t - animation.tStart) / animation.options.duration; 1470 | y = animation.curve(tt); 1471 | properties = {}; 1472 | if (tt >= 1) { 1473 | if (animation.curve.returnsToSelf) { 1474 | properties = animation.properties.start; 1475 | } else { 1476 | properties = animation.properties.end; 1477 | } 1478 | } else { 1479 | _ref = animation.properties.start; 1480 | for (key in _ref) { 1481 | property = _ref[key]; 1482 | properties[key] = interpolate(property, animation.properties.end[key], y); 1483 | } 1484 | } 1485 | applyFrame(animation.el, properties); 1486 | if (typeof (_base = animation.options).change === "function") { 1487 | _base.change(animation.el); 1488 | } 1489 | if (tt >= 1) { 1490 | if (typeof (_base1 = animation.options).complete === "function") { 1491 | _base1.complete(animation.el); 1492 | } 1493 | } 1494 | return tt < 1; 1495 | }; 1496 | 1497 | interpolate = function(start, end, y) { 1498 | if ((start != null) && (start.interpolate != null)) { 1499 | return start.interpolate(end, y); 1500 | } 1501 | return null; 1502 | }; 1503 | 1504 | startAnimation = function(el, properties, options, timeoutId) { 1505 | var endProperties, interpolable, isSVG, k, matrix, startProperties, transforms, v; 1506 | if (timeoutId != null) { 1507 | animationsTimeouts = animationsTimeouts.filter(function(timeout) { 1508 | return timeout.id !== timeoutId; 1509 | }); 1510 | } 1511 | dynamics.stop(el, { 1512 | timeout: false 1513 | }); 1514 | if (!options.animated) { 1515 | dynamics.css(el, properties); 1516 | if (typeof options.complete === "function") { 1517 | options.complete(this); 1518 | } 1519 | return; 1520 | } 1521 | properties = parseProperties(properties); 1522 | startProperties = getCurrentProperties(el, Object.keys(properties)); 1523 | endProperties = {}; 1524 | transforms = []; 1525 | for (k in properties) { 1526 | v = properties[k]; 1527 | if ((el.style != null) && transformProperties.contains(k)) { 1528 | transforms.push([k, v]); 1529 | } else { 1530 | interpolable = createInterpolable(v); 1531 | if (interpolable instanceof InterpolableNumber && (el.style != null)) { 1532 | interpolable = new InterpolableString([interpolable, unitForProperty(k, 0)]); 1533 | } 1534 | endProperties[k] = interpolable; 1535 | } 1536 | } 1537 | if (transforms.length > 0) { 1538 | isSVG = isSVGElement(el); 1539 | if (isSVG) { 1540 | matrix = new Matrix2D(); 1541 | matrix.applyProperties(transforms); 1542 | } else { 1543 | v = (transforms.map(function(transform) { 1544 | return transformValueForProperty(transform[0], transform[1]); 1545 | })).join(" "); 1546 | matrix = Matrix.fromTransform(Matrix.matrixForTransform(v)); 1547 | } 1548 | endProperties['transform'] = matrix.decompose(); 1549 | if (isSVG) { 1550 | startProperties.transform.applyRotateCenter([endProperties.transform.props.rotate[1], endProperties.transform.props.rotate[2]]); 1551 | } 1552 | } 1553 | animations.push({ 1554 | el: el, 1555 | properties: { 1556 | start: startProperties, 1557 | end: endProperties 1558 | }, 1559 | options: options, 1560 | curve: options.type.call(options.type, options) 1561 | }); 1562 | return startMainRunLoop(); 1563 | }; 1564 | 1565 | timeouts = []; 1566 | 1567 | timeoutLastId = 0; 1568 | 1569 | setRealTimeout = function(timeout) { 1570 | if (!isDocumentVisible()) { 1571 | return; 1572 | } 1573 | return timeout.realTimeoutId = setTimeout(function() { 1574 | timeout.fn(); 1575 | return cancelTimeout(timeout.id); 1576 | }, timeout.delay); 1577 | }; 1578 | 1579 | addTimeout = function(fn, delay) { 1580 | var timeout; 1581 | timeoutLastId += 1; 1582 | timeout = { 1583 | id: timeoutLastId, 1584 | tStart: Date.now(), 1585 | fn: fn, 1586 | delay: delay, 1587 | originalDelay: delay 1588 | }; 1589 | setRealTimeout(timeout); 1590 | timeouts.push(timeout); 1591 | return timeoutLastId; 1592 | }; 1593 | 1594 | cancelTimeout = function(id) { 1595 | return timeouts = timeouts.filter(function(timeout) { 1596 | if (timeout.id === id) { 1597 | clearTimeout(timeout.realTimeoutId); 1598 | } 1599 | return timeout.id !== id; 1600 | }); 1601 | }; 1602 | 1603 | leftDelayForTimeout = function(time, timeout) { 1604 | var consumedDelay; 1605 | if (time != null) { 1606 | consumedDelay = time - timeout.tStart; 1607 | return timeout.originalDelay - consumedDelay; 1608 | } else { 1609 | return timeout.originalDelay; 1610 | } 1611 | }; 1612 | 1613 | if (typeof window !== "undefined" && window !== null) { 1614 | window.addEventListener('unload', function() {}); 1615 | } 1616 | 1617 | timeBeforeVisibilityChange = null; 1618 | 1619 | observeVisibilityChange(function(visible) { 1620 | var animation, difference, runloop, timeout, _i, _j, _k, _l, _len, _len1, _len2, _len3, _results; 1621 | runLoopPaused = !visible; 1622 | if (!visible) { 1623 | timeBeforeVisibilityChange = Date.now(); 1624 | _results = []; 1625 | for (_i = 0, _len = timeouts.length; _i < _len; _i++) { 1626 | timeout = timeouts[_i]; 1627 | _results.push(clearTimeout(timeout.realTimeoutId)); 1628 | } 1629 | return _results; 1630 | } else { 1631 | if (runLoopRunning) { 1632 | difference = Date.now() - timeBeforeVisibilityChange; 1633 | for (_j = 0, _len1 = animations.length; _j < _len1; _j++) { 1634 | animation = animations[_j]; 1635 | if (animation.tStart != null) { 1636 | animation.tStart += difference; 1637 | } 1638 | } 1639 | for (_k = 0, _len2 = runloops.length; _k < _len2; _k++) { 1640 | runloop = runloops[_k]; 1641 | if (runloop.tStart != null) { 1642 | runloop.tStart += difference; 1643 | } 1644 | if (runloop.previousT != null) { 1645 | runloop.previousT += difference; 1646 | } 1647 | } 1648 | } 1649 | for (_l = 0, _len3 = timeouts.length; _l < _len3; _l++) { 1650 | timeout = timeouts[_l]; 1651 | timeout.delay = leftDelayForTimeout(timeBeforeVisibilityChange, timeout); 1652 | setRealTimeout(timeout); 1653 | } 1654 | return timeBeforeVisibilityChange = null; 1655 | } 1656 | }); 1657 | 1658 | dynamics = {}; 1659 | 1660 | dynamics.linear = function() { 1661 | return function(t) { 1662 | return t; 1663 | }; 1664 | }; 1665 | 1666 | dynamics.spring = function(options) { 1667 | var A1, A2, decal, frequency, friction, s; 1668 | if (options == null) { 1669 | options = {}; 1670 | } 1671 | applyDefaults(options, dynamics.spring.defaults); 1672 | frequency = Math.max(1, options.frequency / 20); 1673 | friction = Math.pow(20, options.friction / 100); 1674 | s = options.anticipationSize / 1000; 1675 | decal = Math.max(0, s); 1676 | A1 = function(t) { 1677 | var M, a, b, x0, x1; 1678 | M = 0.8; 1679 | x0 = s / (1 - s); 1680 | x1 = 0; 1681 | b = (x0 - (M * x1)) / (x0 - x1); 1682 | a = (M - b) / x0; 1683 | return (a * t * options.anticipationStrength / 100) + b; 1684 | }; 1685 | A2 = function(t) { 1686 | return Math.pow(friction / 10, -t) * (1 - t); 1687 | }; 1688 | return function(t) { 1689 | var A, At, a, angle, b, frictionT, y0, yS; 1690 | frictionT = (t / (1 - s)) - (s / (1 - s)); 1691 | if (t < s) { 1692 | yS = (s / (1 - s)) - (s / (1 - s)); 1693 | y0 = (0 / (1 - s)) - (s / (1 - s)); 1694 | b = Math.acos(1 / A1(yS)); 1695 | a = (Math.acos(1 / A1(y0)) - b) / (frequency * (-s)); 1696 | A = A1; 1697 | } else { 1698 | A = A2; 1699 | b = 0; 1700 | a = 1; 1701 | } 1702 | At = A(frictionT); 1703 | angle = frequency * (t - s) * a + b; 1704 | return 1 - (At * Math.cos(angle)); 1705 | }; 1706 | }; 1707 | 1708 | dynamics.bounce = function(options) { 1709 | var A, fn, frequency, friction; 1710 | if (options == null) { 1711 | options = {}; 1712 | } 1713 | applyDefaults(options, dynamics.bounce.defaults); 1714 | frequency = Math.max(1, options.frequency / 20); 1715 | friction = Math.pow(20, options.friction / 100); 1716 | A = function(t) { 1717 | return Math.pow(friction / 10, -t) * (1 - t); 1718 | }; 1719 | fn = function(t) { 1720 | var At, a, angle, b; 1721 | b = -3.14 / 2; 1722 | a = 1; 1723 | At = A(t); 1724 | angle = frequency * t * a + b; 1725 | return At * Math.cos(angle); 1726 | }; 1727 | fn.returnsToSelf = true; 1728 | return fn; 1729 | }; 1730 | 1731 | dynamics.gravity = function(options) { 1732 | var L, bounciness, curves, elasticity, fn, getPointInCurve, gravity; 1733 | if (options == null) { 1734 | options = {}; 1735 | } 1736 | applyDefaults(options, dynamics.gravity.defaults); 1737 | bounciness = Math.min(options.bounciness / 1250, 0.8); 1738 | elasticity = options.elasticity / 1000; 1739 | gravity = 100; 1740 | curves = []; 1741 | L = (function() { 1742 | var b, curve; 1743 | b = Math.sqrt(2 / gravity); 1744 | curve = { 1745 | a: -b, 1746 | b: b, 1747 | H: 1 1748 | }; 1749 | if (options.returnsToSelf) { 1750 | curve.a = 0; 1751 | curve.b = curve.b * 2; 1752 | } 1753 | while (curve.H > 0.001) { 1754 | L = curve.b - curve.a; 1755 | curve = { 1756 | a: curve.b, 1757 | b: curve.b + L * bounciness, 1758 | H: curve.H * bounciness * bounciness 1759 | }; 1760 | } 1761 | return curve.b; 1762 | })(); 1763 | getPointInCurve = function(a, b, H, t) { 1764 | var c, t2; 1765 | L = b - a; 1766 | t2 = (2 / L) * t - 1 - (a * 2 / L); 1767 | c = t2 * t2 * H - H + 1; 1768 | if (options.returnsToSelf) { 1769 | c = 1 - c; 1770 | } 1771 | return c; 1772 | }; 1773 | (function() { 1774 | var L2, b, curve, _results; 1775 | b = Math.sqrt(2 / (gravity * L * L)); 1776 | curve = { 1777 | a: -b, 1778 | b: b, 1779 | H: 1 1780 | }; 1781 | if (options.returnsToSelf) { 1782 | curve.a = 0; 1783 | curve.b = curve.b * 2; 1784 | } 1785 | curves.push(curve); 1786 | L2 = L; 1787 | _results = []; 1788 | while (curve.b < 1 && curve.H > 0.001) { 1789 | L2 = curve.b - curve.a; 1790 | curve = { 1791 | a: curve.b, 1792 | b: curve.b + L2 * bounciness, 1793 | H: curve.H * elasticity 1794 | }; 1795 | _results.push(curves.push(curve)); 1796 | } 1797 | return _results; 1798 | })(); 1799 | fn = function(t) { 1800 | var curve, i, v; 1801 | i = 0; 1802 | curve = curves[i]; 1803 | while (!(t >= curve.a && t <= curve.b)) { 1804 | i += 1; 1805 | curve = curves[i]; 1806 | if (!curve) { 1807 | break; 1808 | } 1809 | } 1810 | if (!curve) { 1811 | v = options.returnsToSelf ? 0 : 1; 1812 | } else { 1813 | v = getPointInCurve(curve.a, curve.b, curve.H, t); 1814 | } 1815 | return v; 1816 | }; 1817 | fn.returnsToSelf = options.returnsToSelf; 1818 | return fn; 1819 | }; 1820 | 1821 | dynamics.forceWithGravity = function(options) { 1822 | if (options == null) { 1823 | options = {}; 1824 | } 1825 | applyDefaults(options, dynamics.forceWithGravity.defaults); 1826 | options.returnsToSelf = true; 1827 | return dynamics.gravity(options); 1828 | }; 1829 | 1830 | dynamics.bezier = (function() { 1831 | var Bezier, Bezier_, yForX; 1832 | Bezier_ = function(t, p0, p1, p2, p3) { 1833 | return (Math.pow(1 - t, 3) * p0) + (3 * Math.pow(1 - t, 2) * t * p1) + (3 * (1 - t) * Math.pow(t, 2) * p2) + Math.pow(t, 3) * p3; 1834 | }; 1835 | Bezier = function(t, p0, p1, p2, p3) { 1836 | return { 1837 | x: Bezier_(t, p0.x, p1.x, p2.x, p3.x), 1838 | y: Bezier_(t, p0.y, p1.y, p2.y, p3.y) 1839 | }; 1840 | }; 1841 | yForX = function(xTarget, Bs, returnsToSelf) { 1842 | var B, aB, i, lower, percent, upper, x, xTolerance, _i, _len; 1843 | B = null; 1844 | for (_i = 0, _len = Bs.length; _i < _len; _i++) { 1845 | aB = Bs[_i]; 1846 | if (xTarget >= aB(0).x && xTarget <= aB(1).x) { 1847 | B = aB; 1848 | } 1849 | if (B !== null) { 1850 | break; 1851 | } 1852 | } 1853 | if (!B) { 1854 | if (returnsToSelf) { 1855 | return 0; 1856 | } else { 1857 | return 1; 1858 | } 1859 | } 1860 | xTolerance = 0.0001; 1861 | lower = 0; 1862 | upper = 1; 1863 | percent = (upper + lower) / 2; 1864 | x = B(percent).x; 1865 | i = 0; 1866 | while (Math.abs(xTarget - x) > xTolerance && i < 100) { 1867 | if (xTarget > x) { 1868 | lower = percent; 1869 | } else { 1870 | upper = percent; 1871 | } 1872 | percent = (upper + lower) / 2; 1873 | x = B(percent).x; 1874 | i += 1; 1875 | } 1876 | return B(percent).y; 1877 | }; 1878 | return function(options) { 1879 | var Bs, fn, points; 1880 | if (options == null) { 1881 | options = {}; 1882 | } 1883 | points = options.points; 1884 | Bs = (function() { 1885 | var i, k, _fn; 1886 | Bs = []; 1887 | _fn = function(pointA, pointB) { 1888 | var B2; 1889 | B2 = function(t) { 1890 | return Bezier(t, pointA, pointA.cp[pointA.cp.length - 1], pointB.cp[0], pointB); 1891 | }; 1892 | return Bs.push(B2); 1893 | }; 1894 | for (i in points) { 1895 | k = parseInt(i); 1896 | if (k >= points.length - 1) { 1897 | break; 1898 | } 1899 | _fn(points[k], points[k + 1]); 1900 | } 1901 | return Bs; 1902 | })(); 1903 | fn = function(t) { 1904 | if (t === 0) { 1905 | return 0; 1906 | } else if (t === 1) { 1907 | return 1; 1908 | } else { 1909 | return yForX(t, Bs, this.returnsToSelf); 1910 | } 1911 | }; 1912 | fn.returnsToSelf = points[points.length - 1].y === 0; 1913 | return fn; 1914 | }; 1915 | })(); 1916 | 1917 | dynamics.easeInOut = function(options) { 1918 | var friction, _ref; 1919 | if (options == null) { 1920 | options = {}; 1921 | } 1922 | friction = (_ref = options.friction) != null ? _ref : dynamics.easeInOut.defaults.friction; 1923 | return dynamics.bezier({ 1924 | points: [ 1925 | { 1926 | x: 0, 1927 | y: 0, 1928 | cp: [ 1929 | { 1930 | x: 0.92 - (friction / 1000), 1931 | y: 0 1932 | } 1933 | ] 1934 | }, { 1935 | x: 1, 1936 | y: 1, 1937 | cp: [ 1938 | { 1939 | x: 0.08 + (friction / 1000), 1940 | y: 1 1941 | } 1942 | ] 1943 | } 1944 | ] 1945 | }); 1946 | }; 1947 | 1948 | dynamics.easeIn = function(options) { 1949 | var friction, _ref; 1950 | if (options == null) { 1951 | options = {}; 1952 | } 1953 | friction = (_ref = options.friction) != null ? _ref : dynamics.easeIn.defaults.friction; 1954 | return dynamics.bezier({ 1955 | points: [ 1956 | { 1957 | x: 0, 1958 | y: 0, 1959 | cp: [ 1960 | { 1961 | x: 0.92 - (friction / 1000), 1962 | y: 0 1963 | } 1964 | ] 1965 | }, { 1966 | x: 1, 1967 | y: 1, 1968 | cp: [ 1969 | { 1970 | x: 1, 1971 | y: 1 1972 | } 1973 | ] 1974 | } 1975 | ] 1976 | }); 1977 | }; 1978 | 1979 | dynamics.easeOut = function(options) { 1980 | var friction, _ref; 1981 | if (options == null) { 1982 | options = {}; 1983 | } 1984 | friction = (_ref = options.friction) != null ? _ref : dynamics.easeOut.defaults.friction; 1985 | return dynamics.bezier({ 1986 | points: [ 1987 | { 1988 | x: 0, 1989 | y: 0, 1990 | cp: [ 1991 | { 1992 | x: 0, 1993 | y: 0 1994 | } 1995 | ] 1996 | }, { 1997 | x: 1, 1998 | y: 1, 1999 | cp: [ 2000 | { 2001 | x: 0.08 + (friction / 1000), 2002 | y: 1 2003 | } 2004 | ] 2005 | } 2006 | ] 2007 | }); 2008 | }; 2009 | 2010 | dynamics.sinusoidal = function(fn) { 2011 | var ratio; 2012 | ratio = 3.14; 2013 | return function(current, t, dt) { 2014 | var newDT, newT, oldT; 2015 | oldT = Math.sin((t - dt) * ratio); 2016 | newT = Math.sin(t * ratio); 2017 | newDT = newT - oldT; 2018 | return fn(current, newT, newDT); 2019 | }; 2020 | }; 2021 | 2022 | dynamics.easeOut = function(fn) { 2023 | var reset, x; 2024 | x = 0; 2025 | reset = function() { 2026 | return x = 0; 2027 | }; 2028 | return function(current, t, dt) { 2029 | var newDT, newT; 2030 | newT = x + (1 - x) * 0.99 * dt; 2031 | newDT = newT - x; 2032 | x = newT; 2033 | return fn(current, newT, newDT, reset); 2034 | }; 2035 | }; 2036 | 2037 | dynamics.spring.defaults = { 2038 | frequency: 300, 2039 | friction: 200, 2040 | anticipationSize: 0, 2041 | anticipationStrength: 0 2042 | }; 2043 | 2044 | dynamics.bounce.defaults = { 2045 | frequency: 300, 2046 | friction: 200 2047 | }; 2048 | 2049 | dynamics.forceWithGravity.defaults = dynamics.gravity.defaults = { 2050 | bounciness: 400, 2051 | elasticity: 200 2052 | }; 2053 | 2054 | dynamics.easeInOut.defaults = dynamics.easeIn.defaults = dynamics.easeOut.defaults = { 2055 | friction: 500 2056 | }; 2057 | 2058 | dynamics.css = makeArrayFn(function(el, properties) { 2059 | return applyProperties(el, properties, true); 2060 | }); 2061 | 2062 | dynamics.runloop = makeArrayFn(function(el, properties) { 2063 | var currentProperties, endProperties, key, transform, value; 2064 | endProperties = parseProperties(properties); 2065 | currentProperties = getCurrentProperties(el, Object.keys(properties)); 2066 | if (currentProperties.transform != null) { 2067 | transform = currentProperties.transform; 2068 | delete currentProperties.transform; 2069 | for (key in endProperties) { 2070 | value = endProperties[key]; 2071 | if (transformProperties.contains(key)) { 2072 | currentProperties[key] = transform.valueForKey(key); 2073 | } 2074 | } 2075 | } 2076 | runloops.push({ 2077 | el: el, 2078 | properties: { 2079 | current: currentProperties, 2080 | end: endProperties 2081 | } 2082 | }); 2083 | return startMainRunLoop(); 2084 | }); 2085 | 2086 | dynamics.animate = makeArrayFn(function(el, properties, options) { 2087 | var id; 2088 | if (options == null) { 2089 | options = {}; 2090 | } 2091 | options = clone(options); 2092 | applyDefaults(options, { 2093 | type: dynamics.easeInOut, 2094 | duration: 1000, 2095 | delay: 0, 2096 | animated: true 2097 | }); 2098 | options.duration = Math.max(0, options.duration * slowRatio); 2099 | options.delay = Math.max(0, options.delay); 2100 | if (options.delay === 0) { 2101 | return startAnimation(el, properties, options); 2102 | } else { 2103 | id = dynamics.setTimeout(function() { 2104 | return startAnimation(el, properties, options, id); 2105 | }, options.delay); 2106 | return animationsTimeouts.push({ 2107 | id: id, 2108 | el: el 2109 | }); 2110 | } 2111 | }); 2112 | 2113 | dynamics.stop = makeArrayFn(function(el, options) { 2114 | if (options == null) { 2115 | options = {}; 2116 | } 2117 | if (options.timeout == null) { 2118 | options.timeout = true; 2119 | } 2120 | if (options.timeout) { 2121 | animationsTimeouts = animationsTimeouts.filter(function(timeout) { 2122 | if (timeout.el === el && ((options.filter == null) || options.filter(timeout))) { 2123 | dynamics.clearTimeout(timeout.id); 2124 | return false; 2125 | } 2126 | return true; 2127 | }); 2128 | } 2129 | return animations = animations.filter(function(animation) { 2130 | return animation.el !== el; 2131 | }); 2132 | }); 2133 | 2134 | dynamics.setTimeout = function(fn, delay) { 2135 | return addTimeout(fn, delay * slowRatio); 2136 | }; 2137 | 2138 | dynamics.clearTimeout = function(id) { 2139 | return cancelTimeout(id); 2140 | }; 2141 | 2142 | dynamics.toggleSlow = function() { 2143 | slow = !slow; 2144 | if (slow) { 2145 | slowRatio = 3; 2146 | } else { 2147 | slowRatio = 1; 2148 | } 2149 | return typeof console !== "undefined" && console !== null ? typeof console.log === "function" ? console.log("dynamics.js: slow animations " + (slow ? "enabled" : "disabled")) : void 0 : void 0; 2150 | }; 2151 | 2152 | if (typeof module === "object" && typeof module.exports === "object") { 2153 | module.exports = dynamics; 2154 | } else if (typeof define === "function") { 2155 | define('dynamics', function() { 2156 | return dynamics; 2157 | }); 2158 | } else { 2159 | window.dynamics = dynamics; 2160 | } 2161 | 2162 | }).call(this); 2163 | --------------------------------------------------------------------------------