├── 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 | 
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 |
--------------------------------------------------------------------------------