├── .DS_Store ├── public ├── .DS_Store ├── favicon.ico └── index.css ├── .gitignore ├── stylesheets ├── index.styl └── spinners.styl ├── config.coffee ├── lib ├── proxy.coffee ├── room.coffee ├── index.coffee └── get-favorite-cubes.coffee ├── README.md ├── templates └── index.jade ├── models └── artwork.coffee ├── package.json └── index.coffee /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/3d-artsy/master/.DS_Store -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/3d-artsy/master/public/.DS_Store -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/3d-artsy/master/public/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | pids 10 | logs 11 | results 12 | /node_modules 13 | npm-debug.log -------------------------------------------------------------------------------- /stylesheets/index.styl: -------------------------------------------------------------------------------- 1 | @import '../node_modules/nib' 2 | @import './spinners' 3 | 4 | global-reset() 5 | 6 | #main-spinner 7 | position absolute 8 | top 0 9 | left 0 10 | width 100% 11 | height 100% -------------------------------------------------------------------------------- /config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | NODE_ENV: 'development' 3 | ARTSY_ID: '' 4 | ARTSY_SECRET: '' 5 | ARTSY_URL: 'https://api.artsy.net' 6 | REDIS_URL: 'http://localhost:6379' 7 | PORT: 4000 8 | 9 | module.exports[key] = (process.env[key] or val) for key, val of module.exports -------------------------------------------------------------------------------- /lib/proxy.coffee: -------------------------------------------------------------------------------- 1 | url = require("url") 2 | request = require("request") 3 | module.exports = (req, res) -> 4 | url = req.query.url 5 | if url 6 | x = request(url) 7 | req.pipe(x).pipe res 8 | else 9 | res.writeHead 400, 10 | "Content-Type": "text/plain" 11 | res.end "No url" -------------------------------------------------------------------------------- /lib/room.coffee: -------------------------------------------------------------------------------- 1 | THREE = require 'three' 2 | 3 | geometry = new THREE.PlaneGeometry 2000, 2000, 10, 10 4 | geometry.applyMatrix new THREE.Matrix4().makeRotationX( - Math.PI / 2 ) 5 | material = new THREE.MeshBasicMaterial color: 0xf5f5f5 6 | floor = new THREE.Mesh geometry, material 7 | 8 | module.exports = floor -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3d Artsy, a 2014 Artsy Hackathon project 2 | 3 | The source code for a 3d gallery of your Artsy favorites see [3d.artsy.net](http://3d.artsy.net). To see this work locally you'll need to set the `ARTSY_ID` and `ARTSY_SECRET` environment variables to access Artsy's API (currently not public). Email [craig@artsymail.com](mailto:craig@artsymail.com) for questions. 4 | -------------------------------------------------------------------------------- /templates/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Artsy Hackathon 2014 5 | link( type='text/css', rel='stylesheet', href='index.css' ) 6 | body 7 | #main-spinner 8 | .loading-spinner-white 9 | #canvas-container 10 | #scripts 11 | != sharify.script() 12 | script( src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js" ) 13 | script( src='/index.js' ) -------------------------------------------------------------------------------- /models/artwork.coffee: -------------------------------------------------------------------------------- 1 | Backbone = require 'backbone' 2 | { API_URL } = require('sharify').data 3 | 4 | IN_TO_THREE_RATIO = 0.5 5 | 6 | module.exports = class Artwork extends Backbone.Model 7 | 8 | urlRoot: "#{API_URL}/api/v1/artwork/" 9 | 10 | defaultImageUrl: -> 11 | '/proxy?url=' + @get('images')[0].image_url.replace(':version', 'large') 12 | 13 | toCube: -> 14 | width = Math.round parseInt @get('dimensions')['in'].split('×')[1] 15 | height = width / @get('images')[0].aspect_ratio or 16 | Math.round parseInt @get('dimensions')['in'].split('×')[0] 17 | [width * IN_TO_THREE_RATIO, height * IN_TO_THREE_RATIO, 0.2] -------------------------------------------------------------------------------- /stylesheets/spinners.styl: -------------------------------------------------------------------------------- 1 | // 2 | // Styles for loading spinners of all varieties. 3 | // 4 | 5 | @keyframes spin 6 | 0% 7 | transform rotate(0deg) 8 | animation-timing-function linear 9 | 50% 10 | transform rotate(180deg) 11 | animation-timing-function linear 12 | 100% 13 | transform rotate(360deg) 14 | animation-timing-function linear 15 | 16 | spinner(width=25px, height=6px, color=black) 17 | background color 18 | width width 19 | height height 20 | position absolute 21 | top 50% 22 | left 50% 23 | margin-left (-@width / 2) 24 | margin-top (-@height / 2) 25 | animation spin 1s infinite 26 | 27 | .loading-spinner 28 | spinner(25px, 6px) 29 | 30 | .loading-spinner-small 31 | spinner(20px, 4px) 32 | 33 | .loading-spinner-white 34 | @extends .loading-spinner 35 | background white 36 | 37 | .loading-spinner-small-white 38 | @extends .loading-spinner-small 39 | background white -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hack", 3 | "version": "0.0.1", 4 | "description": "Artsy hackathon project 2014.", 5 | "repository": { 6 | "type": "git", 7 | "url": "http://github.com/artsy/benv.git" 8 | }, 9 | "author": { 10 | "name": "Craig Spaeth", 11 | "email": "craigspaeth@gmail.com", 12 | "url": "http://craigspaeth.com" 13 | }, 14 | "engines": { 15 | "node": ">= 0.10.x" 16 | }, 17 | "scripts": { 18 | "test": "make test", 19 | "start": "coffee index.coffee" 20 | }, 21 | "dependencies": { 22 | "artsy-backbone-mixins": "*", 23 | "artsy-xapp": "^1.0.4", 24 | "artsy-xapp-middleware": "*", 25 | "backbone": "*", 26 | "browserify": "*", 27 | "browserify-dev-middleware": "*", 28 | "caching-coffeeify": "*", 29 | "coffee-script": "*", 30 | "express": "*", 31 | "jade": "*", 32 | "jadeify": "*", 33 | "jquery": "^2.2.0", 34 | "nib": "*", 35 | "request": "*", 36 | "sharify": "*", 37 | "stylus": "*", 38 | "three": "*", 39 | "underscore": "*" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | express = require 'express' 2 | sharify = require 'sharify' 3 | imageProxy = require './lib/proxy' 4 | artsyXapp = require 'artsy-xapp' 5 | { NODE_ENV, ARTSY_ID, ARTSY_SECRET, ARTSY_URL, REDIS_URL, PORT } = require './config' 6 | 7 | # App instance 8 | app = module.exports = express() 9 | 10 | # Sharify 11 | sharify.data = 12 | API_URL: ARTSY_URL 13 | NODE_ENV: NODE_ENV 14 | app.use sharify 15 | 16 | # General 17 | app.set 'views', __dirname + '/templates' 18 | app.set 'view engine', 'jade' 19 | 20 | # Asset Middleware 21 | if NODE_ENV is 'development' 22 | app.use require('browserify-dev-middleware') 23 | src: __dirname + '/lib' 24 | transforms: [require('jadeify'), require('caching-coffeeify')] 25 | app.use require("stylus").middleware 26 | src: __dirname + '/stylesheets' 27 | dest: __dirname + '/public' 28 | 29 | # Routes 30 | app.get '/', (req, res) -> 31 | res.locals.sd.XAPP_TOKEN = artsyXapp.token 32 | res.render 'index' 33 | app.use "/proxy", imageProxy 34 | 35 | # Static Middleware 36 | app.use express.static __dirname + '/public' 37 | 38 | # Listen 39 | artsyXapp.init 40 | url: ARTSY_URL 41 | id: ARTSY_ID 42 | secret: ARTSY_SECRET 43 | , -> 44 | app.listen PORT, -> 45 | console.log 'listening on 4000' -------------------------------------------------------------------------------- /lib/index.coffee: -------------------------------------------------------------------------------- 1 | window.THREE = require 'three' 2 | require 'three/examples/js/controls/PointerLockControls.js' 3 | require('backbone').$ = $ 4 | getFavoriteCubes = require './get-favorite-cubes.coffee' 5 | room = require './room.coffee' 6 | { XAPP_TOKEN, ARTWORK_ID } = require('sharify').data 7 | 8 | time = null 9 | 10 | # Add XAPP to headers 11 | $.ajaxSettings.headers = 'X-XAPP-TOKEN' : XAPP_TOKEN 12 | 13 | # Setup scene & renderer 14 | scene = new THREE.Scene() 15 | renderer = new THREE.WebGLRenderer({ antialias: true }) 16 | renderer.setClearColor( 0xffffff ) 17 | renderer.setSize $(window).width(), $(window).height() 18 | 19 | # Setup Camera & Controls 20 | camera = new THREE.PerspectiveCamera 75, $(window).width() / $(window).height(), 0.1, 1000 21 | camera.position.z = 5 22 | controls = new THREE.PointerLockControls camera 23 | scene.add controls.getObject() 24 | 25 | # Lights 26 | light = new THREE.DirectionalLight( 0xffffff, 1.5 ); 27 | light.position.set( 1, 1, 1 ) 28 | scene.add( light ) 29 | 30 | # Append Canvas 31 | init = (cubes) -> 32 | placeWork(cubes) 33 | render() 34 | scene.add room 35 | controls.enabled = true 36 | $('#main-spinner').remove() 37 | 38 | placeWork = (cubes) -> 39 | window.cubes = cubes 40 | for cube, i in cubes 41 | scene.add(cube) 42 | cube.position.x = (Math.random() * 500) - 250 43 | cube.position.z = (Math.random() * 500) - 250 44 | cube.rotation.y = if Math.random() > 0.5 then 1.65 else 0 45 | 46 | render = -> 47 | requestAnimationFrame render 48 | # controls.update Date.now() - time 49 | renderer.render scene, camera 50 | time = Date.now() 51 | 52 | err = (err) -> 53 | console.warn err 54 | 55 | $ -> 56 | $('#canvas-container').html renderer.domElement 57 | getFavoriteCubes location.hash.replace('#','') or 'craig', 58 | error: err 59 | success: (cubes) -> 60 | init(cubes) -------------------------------------------------------------------------------- /lib/get-favorite-cubes.coffee: -------------------------------------------------------------------------------- 1 | # 2 | # Fetches a user's favorites and turns them into three.js cubes 3 | # 4 | # @param {String} id 5 | # @param {Object} options 6 | # 7 | 8 | _ = require 'underscore' 9 | Backbone = require 'backbone' 10 | Artwork = require '../models/artwork.coffee' 11 | THREE = require 'three' 12 | { API_URL } = require('sharify').data 13 | 14 | fetchFavorites = (id, options) -> 15 | $.ajax 16 | url: "#{API_URL}/api/v1/profile/#{id}" 17 | error: options.error 18 | success: (res) -> 19 | artworks = new Backbone.Collection 20 | artworks.model = Artwork 21 | artworks.url = "#{API_URL}/api/v1/collection/saved-artwork/artworks" 22 | artworks.fetch _.extend 23 | data: 24 | sort: '-position' 25 | user_id: res.owner.id 26 | private: true 27 | size: 50 28 | , options 29 | 30 | artworkCube = (artwork) -> 31 | texture = THREE.ImageUtils.loadTexture(artwork.defaultImageUrl(), THREE.UVMapping) 32 | material = new THREE.MeshBasicMaterial color: 0xffffff, map: texture 33 | geometry = new THREE.CubeGeometry artwork.toCube()... 34 | mesh = new THREE.Mesh geometry, material 35 | mesh.position.y = artwork.toCube()[1] / 2 + 2 36 | wallHeight = 70 37 | wallPadding = 10 38 | geometry = new THREE.CubeGeometry( 39 | artwork.toCube()[0] + wallPadding, 40 | wallHeight = (artwork.toCube()[1] + wallPadding), 41 | 1.5 42 | ) 43 | material = new THREE.MeshBasicMaterial color: 0xffffff 44 | wall = new THREE.Mesh geometry, material 45 | wall.position.z = -1 46 | wall.position.y = 0 47 | mesh.add wall 48 | mesh.artwork = artwork 49 | mesh 50 | 51 | module.exports = (id, options) -> 52 | fetchFavorites id, 53 | error: options.error 54 | success: (favorites) -> 55 | options.success _.map favorites.filter((artwork) -> artwork.get('dimensions')?.in), artworkCube -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | .loading-spinner, 2 | .loading-spinner-white { 3 | background: #000; 4 | width: 25px; 5 | height: 6px; 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | margin-left: -12.5px; 10 | margin-top: -3px; 11 | -webkit-animation: spin 1s infinite; 12 | -moz-animation: spin 1s infinite; 13 | -o-animation: spin 1s infinite; 14 | -ms-animation: spin 1s infinite; 15 | animation: spin 1s infinite; 16 | } 17 | .loading-spinner-small, 18 | .loading-spinner-small-white { 19 | background: #000; 20 | width: 20px; 21 | height: 4px; 22 | position: absolute; 23 | top: 50%; 24 | left: 50%; 25 | margin-left: -10px; 26 | margin-top: -2px; 27 | -webkit-animation: spin 1s infinite; 28 | -moz-animation: spin 1s infinite; 29 | -o-animation: spin 1s infinite; 30 | -ms-animation: spin 1s infinite; 31 | animation: spin 1s infinite; 32 | } 33 | .loading-spinner-white { 34 | background: #fff; 35 | } 36 | .loading-spinner-small-white { 37 | background: #fff; 38 | } 39 | @-moz-keyframes spin { 40 | 0% { 41 | -webkit-transform: rotate(0deg); 42 | -moz-transform: rotate(0deg); 43 | -o-transform: rotate(0deg); 44 | -ms-transform: rotate(0deg); 45 | transform: rotate(0deg); 46 | -webkit-animation-timing-function: linear; 47 | -moz-animation-timing-function: linear; 48 | -o-animation-timing-function: linear; 49 | -ms-animation-timing-function: linear; 50 | animation-timing-function: linear; 51 | } 52 | 50% { 53 | -webkit-transform: rotate(180deg); 54 | -moz-transform: rotate(180deg); 55 | -o-transform: rotate(180deg); 56 | -ms-transform: rotate(180deg); 57 | transform: rotate(180deg); 58 | -webkit-animation-timing-function: linear; 59 | -moz-animation-timing-function: linear; 60 | -o-animation-timing-function: linear; 61 | -ms-animation-timing-function: linear; 62 | animation-timing-function: linear; 63 | } 64 | 100% { 65 | -webkit-transform: rotate(360deg); 66 | -moz-transform: rotate(360deg); 67 | -o-transform: rotate(360deg); 68 | -ms-transform: rotate(360deg); 69 | transform: rotate(360deg); 70 | -webkit-animation-timing-function: linear; 71 | -moz-animation-timing-function: linear; 72 | -o-animation-timing-function: linear; 73 | -ms-animation-timing-function: linear; 74 | animation-timing-function: linear; 75 | } 76 | } 77 | @-webkit-keyframes spin { 78 | 0% { 79 | -webkit-transform: rotate(0deg); 80 | -moz-transform: rotate(0deg); 81 | -o-transform: rotate(0deg); 82 | -ms-transform: rotate(0deg); 83 | transform: rotate(0deg); 84 | -webkit-animation-timing-function: linear; 85 | -moz-animation-timing-function: linear; 86 | -o-animation-timing-function: linear; 87 | -ms-animation-timing-function: linear; 88 | animation-timing-function: linear; 89 | } 90 | 50% { 91 | -webkit-transform: rotate(180deg); 92 | -moz-transform: rotate(180deg); 93 | -o-transform: rotate(180deg); 94 | -ms-transform: rotate(180deg); 95 | transform: rotate(180deg); 96 | -webkit-animation-timing-function: linear; 97 | -moz-animation-timing-function: linear; 98 | -o-animation-timing-function: linear; 99 | -ms-animation-timing-function: linear; 100 | animation-timing-function: linear; 101 | } 102 | 100% { 103 | -webkit-transform: rotate(360deg); 104 | -moz-transform: rotate(360deg); 105 | -o-transform: rotate(360deg); 106 | -ms-transform: rotate(360deg); 107 | transform: rotate(360deg); 108 | -webkit-animation-timing-function: linear; 109 | -moz-animation-timing-function: linear; 110 | -o-animation-timing-function: linear; 111 | -ms-animation-timing-function: linear; 112 | animation-timing-function: linear; 113 | } 114 | } 115 | @-o-keyframes spin { 116 | 0% { 117 | -webkit-transform: rotate(0deg); 118 | -moz-transform: rotate(0deg); 119 | -o-transform: rotate(0deg); 120 | -ms-transform: rotate(0deg); 121 | transform: rotate(0deg); 122 | -webkit-animation-timing-function: linear; 123 | -moz-animation-timing-function: linear; 124 | -o-animation-timing-function: linear; 125 | -ms-animation-timing-function: linear; 126 | animation-timing-function: linear; 127 | } 128 | 50% { 129 | -webkit-transform: rotate(180deg); 130 | -moz-transform: rotate(180deg); 131 | -o-transform: rotate(180deg); 132 | -ms-transform: rotate(180deg); 133 | transform: rotate(180deg); 134 | -webkit-animation-timing-function: linear; 135 | -moz-animation-timing-function: linear; 136 | -o-animation-timing-function: linear; 137 | -ms-animation-timing-function: linear; 138 | animation-timing-function: linear; 139 | } 140 | 100% { 141 | -webkit-transform: rotate(360deg); 142 | -moz-transform: rotate(360deg); 143 | -o-transform: rotate(360deg); 144 | -ms-transform: rotate(360deg); 145 | transform: rotate(360deg); 146 | -webkit-animation-timing-function: linear; 147 | -moz-animation-timing-function: linear; 148 | -o-animation-timing-function: linear; 149 | -ms-animation-timing-function: linear; 150 | animation-timing-function: linear; 151 | } 152 | } 153 | @keyframes spin { 154 | 0% { 155 | -webkit-transform: rotate(0deg); 156 | -moz-transform: rotate(0deg); 157 | -o-transform: rotate(0deg); 158 | -ms-transform: rotate(0deg); 159 | transform: rotate(0deg); 160 | -webkit-animation-timing-function: linear; 161 | -moz-animation-timing-function: linear; 162 | -o-animation-timing-function: linear; 163 | -ms-animation-timing-function: linear; 164 | animation-timing-function: linear; 165 | } 166 | 50% { 167 | -webkit-transform: rotate(180deg); 168 | -moz-transform: rotate(180deg); 169 | -o-transform: rotate(180deg); 170 | -ms-transform: rotate(180deg); 171 | transform: rotate(180deg); 172 | -webkit-animation-timing-function: linear; 173 | -moz-animation-timing-function: linear; 174 | -o-animation-timing-function: linear; 175 | -ms-animation-timing-function: linear; 176 | animation-timing-function: linear; 177 | } 178 | 100% { 179 | -webkit-transform: rotate(360deg); 180 | -moz-transform: rotate(360deg); 181 | -o-transform: rotate(360deg); 182 | -ms-transform: rotate(360deg); 183 | transform: rotate(360deg); 184 | -webkit-animation-timing-function: linear; 185 | -moz-animation-timing-function: linear; 186 | -o-animation-timing-function: linear; 187 | -ms-animation-timing-function: linear; 188 | animation-timing-function: linear; 189 | } 190 | } 191 | html, 192 | body, 193 | div, 194 | span, 195 | applet, 196 | object, 197 | iframe, 198 | h1, 199 | h2, 200 | h3, 201 | h4, 202 | h5, 203 | h6, 204 | p, 205 | blockquote, 206 | pre, 207 | a, 208 | abbr, 209 | acronym, 210 | address, 211 | big, 212 | cite, 213 | code, 214 | del, 215 | dfn, 216 | em, 217 | img, 218 | ins, 219 | kbd, 220 | q, 221 | s, 222 | samp, 223 | small, 224 | strike, 225 | strong, 226 | sub, 227 | sup, 228 | tt, 229 | var, 230 | dl, 231 | dt, 232 | dd, 233 | ol, 234 | ul, 235 | li, 236 | fieldset, 237 | form, 238 | label, 239 | legend, 240 | table, 241 | caption, 242 | tbody, 243 | tfoot, 244 | thead, 245 | tr, 246 | th, 247 | td { 248 | margin: 0; 249 | padding: 0; 250 | border: 0; 251 | outline: 0; 252 | font-weight: inherit; 253 | font-style: inherit; 254 | font-family: inherit; 255 | font-size: 100%; 256 | vertical-align: baseline; 257 | } 258 | body { 259 | line-height: 1; 260 | color: #000; 261 | background: #fff; 262 | } 263 | ol, 264 | ul { 265 | list-style: none; 266 | } 267 | table { 268 | border-collapse: separate; 269 | border-spacing: 0; 270 | vertical-align: middle; 271 | } 272 | caption, 273 | th, 274 | td { 275 | text-align: left; 276 | font-weight: normal; 277 | vertical-align: middle; 278 | } 279 | a img { 280 | border: none; 281 | } 282 | #main-spinner { 283 | position: absolute; 284 | top: 0; 285 | left: 0; 286 | width: 100%; 287 | height: 100%; 288 | } 289 | --------------------------------------------------------------------------------