├── .gitignore ├── chat.js ├── index.js ├── package.json ├── randomname.js ├── readme.md └── www ├── demo.js ├── hello-world.js ├── index.html ├── logo-white.png ├── player.png ├── ratchet.css ├── share-game.js ├── share.html ├── share.js ├── textures ├── brick.png ├── crate.png ├── dirt.png ├── grass.png ├── grass_dirt.png ├── obsidian.png ├── snow.png └── whitewool.png └── viking.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /chat.js: -------------------------------------------------------------------------------- 1 | module.exports = function(name, emitter) { 2 | // Handle entering a command 3 | window.addEventListener('keyup', function(e) { 4 | if (e.keyCode !== 13) return; 5 | var el = document.getElementById('cmd'); 6 | if (document.activeElement === el) { 7 | emitter.emit('message', {user: name, text: el.value}) 8 | el.value = ''; 9 | el.blur(); 10 | } else { 11 | el.focus(); 12 | } 13 | }); 14 | 15 | emitter.on('message', showMessage) 16 | 17 | function showMessage(message) { 18 | var li = document.createElement('li') 19 | li.innerHTML = message.user + ': ' + message.text 20 | messages.appendChild(li) 21 | messages.scrollTop = messages.scrollHeight 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | var websocket = require('websocket-stream') 3 | var engine = require('voxel-engine') 4 | var duplexEmitter = require('duplex-emitter') 5 | var toolbar = require('toolbar') 6 | var randomName = require('./randomname') 7 | var crunch = require('voxel-crunch') 8 | var emitChat = require('./chat') 9 | var highlight = require('voxel-highlight') 10 | var skin = require('minecraft-skin') 11 | var player = require('voxel-player') 12 | var texturePath = "/textures/" 13 | //var game 14 | 15 | module.exports = Client 16 | 17 | function Client(server, game) { 18 | if(!(this instanceof Client)) { 19 | return new Client(server, game) 20 | } 21 | // this.blockSelector = toolbar({el: '#tools'}) 22 | this.playerID 23 | this.lastProcessedSeq = 0 24 | this.localInputs = [] 25 | this.connected = false 26 | this.currentMaterial = 1 27 | this.lerpPercent = 0.1 28 | this.server = server || 'ws://' + url.parse(window.location.href).host 29 | this.others = {} 30 | this.connect(server, game) 31 | this.game 32 | window.others = this.others 33 | } 34 | 35 | Client.prototype.connect = function(server, game) { 36 | var self = this 37 | var socket = websocket(server) 38 | socket.on('end', function() { self.connected = false }) 39 | this.socket = socket 40 | this.bindEvents(socket, game) 41 | } 42 | 43 | Client.prototype.bindEvents = function(socket, game) { 44 | var self = this 45 | this.emitter = duplexEmitter(socket) 46 | var emitter = this.emitter 47 | this.connected = true 48 | 49 | emitter.on('id', function(id) { 50 | console.log('got id', id) 51 | self.playerID = id 52 | if (game != null) { 53 | self.game = game 54 | console.log("Sending local settings to the server.") 55 | emitter.emit('clientSettings', self.game.settings) 56 | } else { 57 | emitter.emit('clientSettings', null) 58 | } 59 | }) 60 | 61 | emitter.on('settings', function(settings) { 62 | settings.texturePath = texturePath 63 | settings.generateChunks = false 64 | //deserialise the voxel.generator function. 65 | if (settings.generatorToString != null) { 66 | settings.generate = eval("(" + settings.generatorToString + ")") 67 | } 68 | self.game = self.createGame(settings, game) 69 | emitter.emit('created') 70 | emitter.on('chunk', function(encoded, chunk) { 71 | var voxels = crunch.decode(encoded, new Uint32Array(chunk.length)) 72 | chunk.voxels = voxels 73 | self.game.showChunk(chunk) 74 | }) 75 | }) 76 | 77 | // fires when server sends us voxel edits 78 | emitter.on('set', function(pos, val) { 79 | self.game.setBlock(pos, val) 80 | }) 81 | } 82 | 83 | Client.prototype.createGame = function(settings, game) { 84 | var self = this 85 | var emitter = this.emitter 86 | settings.controlsDisabled = false 87 | self.game = engine(settings) 88 | self.game.settings = settings 89 | function sendState() { 90 | if (!self.connected) return 91 | var player = self.game.controls.target() 92 | var state = { 93 | position: player.yaw.position, 94 | rotation: { 95 | y: player.yaw.rotation.y, 96 | x: player.pitch.rotation.x 97 | } 98 | } 99 | emitter.emit('state', state) 100 | } 101 | 102 | var name = localStorage.getItem('name') 103 | if (!name) { 104 | name = randomName() 105 | localStorage.setItem('name', name) 106 | } 107 | 108 | self.game.controls.on('data', function(state) { 109 | var interacting = false 110 | Object.keys(state).map(function(control) { 111 | if (state[control] > 0) interacting = true 112 | }) 113 | if (interacting) sendState() 114 | }) 115 | 116 | emitChat(name, emitter) 117 | 118 | // setTimeout is because three.js seems to throw errors if you add stuff too soon 119 | setTimeout(function() { 120 | emitter.on('update', function(updates) { 121 | Object.keys(updates.positions).map(function(player) { 122 | var update = updates.positions[player] 123 | if (player === self.playerID) return self.onServerUpdate(update) // local player 124 | self.updatePlayerPosition(player, update) // other players 125 | }) 126 | }) 127 | }, 1000) 128 | 129 | emitter.on('leave', function(id) { 130 | if (!self.others[id]) return 131 | self.game.scene.remove(self.others[id].mesh) 132 | delete self.others[id] 133 | }) 134 | 135 | return self.game 136 | } 137 | 138 | Client.prototype.onServerUpdate = function(update) { 139 | // todo use server sent location 140 | } 141 | 142 | Client.prototype.lerpMe = function(position) { 143 | var to = new this.game.THREE.Vector3() 144 | to.copy(position) 145 | var from = this.game.controls.target().yaw.position 146 | from.copy(from.lerp(to, this.lerpPercent)) 147 | } 148 | 149 | Client.prototype.updatePlayerPosition = function(id, update) { 150 | var pos = update.position 151 | var player = this.others[id] 152 | if (!player) { 153 | var playerSkin = skin(this.game.THREE, 'player.png', { 154 | scale: new this.game.THREE.Vector3(0.04, 0.04, 0.04) 155 | }) 156 | var playerMesh = playerSkin.mesh 157 | this.others[id] = playerSkin 158 | playerMesh.children[0].position.y = 10 159 | this.game.scene.add(playerMesh) 160 | } 161 | var playerSkin = this.others[id] 162 | var playerMesh = playerSkin.mesh 163 | playerMesh.position.copy(playerMesh.position.lerp(pos, this.lerpPercent)) 164 | 165 | // playerMesh.position.y += 17 166 | playerMesh.children[0].rotation.y = update.rotation.y + (Math.PI / 2) 167 | playerSkin.head.rotation.z = scale(update.rotation.x, -1.5, 1.5, -0.75, 0.75) 168 | } 169 | 170 | function scale( x, fromLow, fromHigh, toLow, toHigh ) { 171 | return ( x - fromLow ) * ( toHigh - toLow ) / ( fromHigh - fromLow ) + toLow 172 | } 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voxel-client", 3 | "description": "multiplayer client for voxel-server", 4 | "version": "0.0.1", 5 | "dependencies": { 6 | "voxel-engine": "0.16.x", 7 | "websocket-stream": "0.0.5", 8 | "ws": "0.4.25", 9 | "hat": "0.0.3", 10 | "duplex-emitter": "~0.1.9", 11 | "voxel-crunch": "~0.2.1", 12 | "toolbar": "0.0.5", 13 | "voxel-highlight": "0.0.8", 14 | "minecraft-skin": "0.1.2", 15 | "voxel-player": "0.1.0", 16 | "extend": "1.1.3", 17 | "voxel": "0.3.1" 18 | }, 19 | "engines": { 20 | "node": "0.8.x" 21 | }, 22 | "scripts": { 23 | "start": "cd www && beefy demo.js:bundle.js" 24 | }, 25 | "devDependencies": { 26 | "browserify": "~2.10.2", 27 | "beefy": "0.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /randomname.js: -------------------------------------------------------------------------------- 1 | var names = ['alpha', 'space', 'beta', 'omega', 'zone', 'base', 'bass', 'centaur', 'official', 'free', 'fresh', 'freedom', 'power', 'synth', 'lazer', 'gamma', 'red', 'green', 'ultra', 'net', 'cafe', 'secret', 'open', 'track', 'wisdom', 'genghis', 'tron', 'grid', 'plus', 'maximum', 'pre', 'post', 'mid', 'pyramid', 'core', 'fast', 'dragon', 'wizard', 'yak', 'crystal', 'electric', 'rizzle', 'tiny', 'pantry', 'dry', 'bleeding', 'fruit', 'mower', 'whiskey', 'marble', 'cake', 'meat', 'donkey','wewo', 'banana', 'rooster', 'bear', 'bacon', 'moon', 'glitter', 'grandma', 'walrus', 'party', 'junk', 'crazy', 'turbo', 'hyper', 'mega', 'boss', 'dunk', 'pow', 'icy', 'bobo', 'pile', 'tater', 'wiz', 'gnarl', 'mix', 'pop', 'mustard', 'bizzler', 'fog', 'punch', 'planar'] 2 | 3 | module.exports = function(len) { 4 | len = len || 2 5 | var name = '' 6 | while (len > 0) { 7 | name += randomName() 8 | len-- 9 | } 10 | return name 11 | } 12 | 13 | function randomName() { 14 | return names[~~(Math.random() * names.length)] 15 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # voxel-client 2 | 3 | Enable an app based on voxel-engine to be a client for [voxel-server](https://github.com/maxogden/voxel-server) 4 | 5 | # Example 6 | 7 | var client = createClient(opts.server || "ws://localhost:8080/") 8 | 9 | # Run the demo 10 | 11 | Run the start script: 12 | 13 | ``` 14 | npm start 15 | ``` 16 | 17 | This launches demo.js using beefy on port 9966. 18 | 19 | Modify demo.js and replace hello-world.js with share-game.js to view how to share a game. 20 | 21 | # Integrate it with your game 22 | 23 | Follow the instructions for [voxel-server](https://github.com/maxogden/voxel-server) and get it running. 24 | 25 | You can delete any lines in your app that create settings or the game; the server emits the settings to the client(s) and 26 | voxel-client creates the instance of the game. View www/hello-world.js for a working example. 27 | 28 | Replace the settings in Voxel-server if you wish your game to have different settings. 29 | 30 | Use avatarInitialPosition to set the initial position of your avatar, based on 31 | settings sent from the server. If you don't use these, you might end up in the middle of some blocks! 32 | 33 | ``` js 34 | var settings = game.settings.avatarInitialPosition 35 | avatar.position.set(settings[0],settings[1],settings[2]) 36 | ``` 37 | # Sharing your own game 38 | 39 | If you wish to share a game you've created but don't want to modify the voxel-server code, view www/share-game.js for an example. 40 | 41 | To run the share demo, run 42 | 43 | beefy share.js:share-bundle.js 9968 44 | 45 | and view it at http://localhost:9968/share.html 46 | 47 | Remove the code that creates an instance of game (var game = createGame(opts)). Create an empty game object and populate it with your settings. 48 | If you are using a generator function from voxel, serialize it and add it to the generatorToString settings property. 49 | 50 | ``` js 51 | var settings = { 52 | generate: voxel.generator['Valley'], 53 | chunkDistance: 2, 54 | materials: [ 55 | ['grass', 'dirt', 'grass_dirt'], 56 | 'obsidian', 57 | 'brick', 58 | 'grass' 59 | ], 60 | texturePath: texturePath, 61 | worldOrigin: [0, 0, 0], 62 | controls: { discreteFire: true }, 63 | avatarInitialPosition: [2, 20, 2], // sets the avatar in the right place. 64 | resetSettings: true // When you want to force the server to reset the game. 65 | } 66 | 67 | var game = {} 68 | ``` 69 | If you are using a generator function from voxel, serialize it and add it to the generatorToString settings property. 70 | 71 | settings.generatorToString = settings.generate.toString() 72 | 73 | Add settings to this empty game object 74 | 75 | game.settings = settings 76 | 77 | Add game to the createClient paramaeters: 78 | 79 | var client = createClient(opts.server || "ws://localhost:8080/", game) 80 | 81 | # API 82 | 83 | ## createClient(server, game) 84 | 85 | server is typically "ws://localhost:8080/" 86 | 87 | If game is null, voxel-client will create a game instance using settings from the server. 88 | 89 | 90 | BSD LICENSE -------------------------------------------------------------------------------- /www/demo.js: -------------------------------------------------------------------------------- 1 | require('./hello-world.js')({server: "ws://localhost:8080/"}) -------------------------------------------------------------------------------- /www/hello-world.js: -------------------------------------------------------------------------------- 1 | var createClient = require('../') 2 | var highlight = require('voxel-highlight') 3 | var extend = require('extend') 4 | var voxelPlayer = require('voxel-player') 5 | var game 6 | 7 | module.exports = function(opts, setup) { 8 | setup = setup || defaultSetup 9 | opts = extend({}, opts || {}) 10 | 11 | var client = createClient(opts.server || "ws://localhost:8080/") 12 | 13 | client.emitter.on('noMoreChunks', function(id) { 14 | console.log("Attaching to the container and creating player") 15 | var container = opts.container || document.body 16 | game = client.game 17 | game.appendTo(container) 18 | if (game.notCapable()) return game 19 | var createPlayer = voxelPlayer(game) 20 | 21 | // create the player from a minecraft skin file and tell the 22 | // game to use it as the main player 23 | var avatar = createPlayer('player.png') 24 | window.avatar = avatar 25 | avatar.possess() 26 | var settings = game.settings.avatarInitialPosition 27 | avatar.position.set(settings[0],settings[1],settings[2]) 28 | setup(game, avatar, client) 29 | }) 30 | 31 | return game 32 | } 33 | 34 | function defaultSetup(game, avatar, client) { 35 | // highlight blocks when you look at them, hold for block placement 36 | var blockPosPlace, blockPosErase 37 | var hl = game.highlighter = highlight(game, { color: 0xff0000 }) 38 | hl.on('highlight', function (voxelPos) { blockPosErase = voxelPos }) 39 | hl.on('remove', function (voxelPos) { blockPosErase = null }) 40 | hl.on('highlight-adjacent', function (voxelPos) { blockPosPlace = voxelPos }) 41 | hl.on('remove-adjacent', function (voxelPos) { blockPosPlace = null }) 42 | 43 | // toggle between first and third person modes 44 | window.addEventListener('keydown', function (ev) { 45 | if (ev.keyCode === 'R'.charCodeAt(0)) avatar.toggle() 46 | }) 47 | 48 | // block interaction stuff, uses highlight data 49 | var currentMaterial = 1 50 | 51 | game.on('fire', function (target, state) { 52 | var position = blockPosPlace 53 | if (position) { 54 | game.createBlock(position, currentMaterial) 55 | client.emitter.emit('set', position, currentMaterial) 56 | } else { 57 | position = blockPosErase 58 | if (position) { 59 | game.setBlock(position, 0) 60 | console.log("Erasing point at " + JSON.stringify(position)) 61 | client.emitter.emit('set', position, 0) 62 | } 63 | } 64 | }) 65 | } -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Voxels 5 | 6 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /www/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/logo-white.png -------------------------------------------------------------------------------- /www/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/player.png -------------------------------------------------------------------------------- /www/ratchet.css: -------------------------------------------------------------------------------- 1 | /** 2 | * ================================== 3 | * Ratchet v1.0.0 4 | * Licensed under The MIT License 5 | * http://opensource.org/licenses/MIT 6 | * ================================== 7 | */ 8 | 9 | /* Hard reset 10 | -------------------------------------------------- */ 11 | 12 | html, 13 | body, 14 | div, 15 | span, 16 | iframe, 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6, 23 | p, 24 | blockquote, 25 | pre, 26 | a, 27 | abbr, 28 | acronym, 29 | address, 30 | big, 31 | cite, 32 | code, 33 | del, 34 | dfn, 35 | em, 36 | img, 37 | ins, 38 | kbd, 39 | q, 40 | s, 41 | samp, 42 | small, 43 | strike, 44 | strong, 45 | sub, 46 | sup, 47 | tt, 48 | var, 49 | b, 50 | u, 51 | i, 52 | center, 53 | dl, 54 | dt, 55 | dd, 56 | ol, 57 | ul, 58 | li, 59 | fieldset, 60 | form, 61 | label, 62 | legend, 63 | table, 64 | caption, 65 | tbody, 66 | tfoot, 67 | thead, 68 | tr, 69 | th, 70 | td, 71 | article, 72 | aside, 73 | canvas, 74 | details, 75 | embed, 76 | figure, 77 | figcaption, 78 | footer, 79 | header, 80 | hgroup, 81 | menu, 82 | nav, 83 | output, 84 | section, 85 | summary, 86 | time, 87 | audio, 88 | video { 89 | padding: 0; 90 | margin: 0; 91 | border: 0; 92 | } 93 | 94 | /* Prevents iOS text size adjust after orientation change, without disabling (Thanks to @necolas) */ 95 | html { 96 | -webkit-text-size-adjust: 100%; 97 | -ms-text-size-adjust: 100%; 98 | } 99 | 100 | /* Base styles 101 | -------------------------------------------------- */ 102 | 103 | body { 104 | position: fixed; 105 | top: 0; 106 | right: 0; 107 | bottom: 0; 108 | left: 0; 109 | font: 14px/1.25 "Helvetica Neue", sans-serif; 110 | color: #222; 111 | background-color: #fff; 112 | } 113 | 114 | /* Universal link styling */ 115 | a { 116 | color: #0882f0; 117 | text-decoration: none; 118 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); /* Removes the dark touch outlines on links */ 119 | } 120 | 121 | /* Wrapper to be used around all content not in .bar-title and .bar-tab */ 122 | .content { 123 | position: fixed; 124 | top: 0; 125 | right: 0; 126 | bottom: 0; 127 | left: 0; 128 | overflow: auto; 129 | background-color: #fff; 130 | -webkit-transition-property: top, bottom; 131 | transition-property: top, bottom; 132 | -webkit-transition-duration: .2s, .2s; 133 | transition-duration: .2s, .2s; 134 | -webkit-transition-timing-function: linear, linear; 135 | transition-timing-function: linear, linear; 136 | -webkit-overflow-scrolling: touch; 137 | } 138 | 139 | /* Hack to force all relatively and absolutely positioned elements still render while scrolling 140 | Note: This is a bug for "-webkit-overflow-scrolling: touch" */ 141 | .content > * { 142 | -webkit-transform: translateZ(0px); 143 | transform: translateZ(0px); 144 | } 145 | 146 | /* Utility wrapper to pad in components like forms, block buttons and segmented-controllers so they're not full-bleed */ 147 | .content-padded { 148 | padding: 10px; 149 | } 150 | 151 | /* Pad top/bottom of content so it doesn't hide behind .bar-title and .bar-tab. 152 | Note: For these to work, content must come after both bars in the markup */ 153 | .bar-title ~ .content { 154 | top: 44px; 155 | } 156 | .bar-tab ~ .content { 157 | bottom: 51px; 158 | } 159 | .bar-header-secondary ~ .content { 160 | top: 88px; 161 | }/* General bar styles 162 | -------------------------------------------------- */ 163 | 164 | [class*="bar-"] { 165 | position: fixed; 166 | right: 0; 167 | left: 0; 168 | z-index: 10; 169 | height: 44px; 170 | padding: 5px; 171 | box-sizing: border-box; 172 | } 173 | 174 | /* Modifier class to dock any bar below .bar-title */ 175 | .bar-header-secondary { 176 | top: 45px; 177 | } 178 | 179 | /* Modifier class to dock any bar to bottom of viewport */ 180 | .bar-footer { 181 | bottom: 0; 182 | } 183 | 184 | /* Generic bar for wrapping buttons, segmented controllers, etc. */ 185 | .bar-standard { 186 | background-color: #f2f2f2; 187 | background-image: -webkit-linear-gradient(top, #f2f2f2 0, #e5e5e5 100%); 188 | background-image: linear-gradient(to bottom, #f2f2f2 0, #e5e5e5 100%); 189 | border-bottom: 1px solid #aaa; 190 | box-shadow: inset 0 1px 1px -1px #fff; 191 | } 192 | 193 | /* Flip border position to top for footer bars */ 194 | .bar-footer.bar-standard, 195 | .bar-footer-secondary.bar-standard { 196 | border-top: 1px solid #aaa; 197 | border-bottom-width: 0; 198 | } 199 | 200 | /* Title bar 201 | -------------------------------------------------- */ 202 | 203 | /* Bar docked to top of viewport for showing page title and actions */ 204 | .bar-title { 205 | top: 0; 206 | display: -webkit-box; 207 | display: box; 208 | background-color: #1eb0e9; 209 | background-image: -webkit-linear-gradient(top, #1eb0e9 0, #109adc 100%); 210 | background-image: linear-gradient(to bottom, #1eb0e9 0, #109adc 100%); 211 | border-bottom: 1px solid #0e5895; 212 | box-shadow: inset 0 1px 1px -1px rgba(255, 255, 255, .8); 213 | -webkit-box-orient: horizontal; 214 | box-orient: horizontal; 215 | } 216 | 217 | /* Centered text in the .bar-title */ 218 | .bar-title .title { 219 | position: absolute; 220 | top: 0; 221 | left: 0; 222 | display: block; 223 | width: 100%; 224 | font-size: 20px; 225 | font-weight: bold; 226 | line-height: 44px; 227 | color: #fff; 228 | text-align: center; 229 | text-shadow: 0 -1px rgba(0, 0, 0, .5); 230 | white-space: nowrap; 231 | } 232 | 233 | .bar-title > a:not([class*="button"]) { 234 | display: block; 235 | width: 100%; 236 | height: 100%; 237 | } 238 | 239 | /* Retain specified title color */ 240 | .bar-title .title a { 241 | color: inherit; 242 | } 243 | 244 | /* Tab bar 245 | -------------------------------------------------- */ 246 | 247 | /* Bar docked to bottom used for primary app navigation */ 248 | .bar-tab { 249 | bottom: 0; 250 | height: 50px; 251 | padding: 0; 252 | background-color: #393939; 253 | background-image: -webkit-linear-gradient(top, #393939 0, #2b2b2b 100%); 254 | background-image: linear-gradient(to bottom, #393939 0, #2b2b2b 100%); 255 | border-top: 1px solid #000; 256 | border-bottom-width: 0; 257 | box-shadow: inset 0 1px 1px -1px rgba(255, 255, 255, .6); 258 | } 259 | 260 | /* Wrapper for individual tab */ 261 | .tab-inner { 262 | display: -webkit-box; 263 | display: box; 264 | height: 100%; 265 | list-style: none; 266 | -webkit-box-orient: horizontal; 267 | box-orient: horizontal; 268 | } 269 | 270 | /* Navigational tab */ 271 | .tab-item { 272 | height: 100%; 273 | padding-top: 9px; 274 | text-align: center; 275 | box-sizing: border-box; 276 | -webkit-box-flex: 1; 277 | box-flex: 1; 278 | } 279 | 280 | /* Active state for tab */ 281 | .tab-item.active { 282 | box-shadow: inset 0 0 20px rgba(0, 0, 0, .5); 283 | } 284 | 285 | /* Icon for tab */ 286 | .tab-icon { 287 | display: block; 288 | height: 18px; 289 | margin: 0 auto; 290 | } 291 | 292 | /* Label for tab */ 293 | .tab-label { 294 | margin-top: 1px; 295 | font-size: 10px; 296 | font-weight: bold; 297 | color: #fff; 298 | text-shadow: 0 1px rgba(0, 0, 0, .3); 299 | } 300 | 301 | /* Buttons in title bars 302 | -------------------------------------------------- */ 303 | 304 | /* Generic style for all buttons in .bar-title */ 305 | .bar-title [class*="button"] { 306 | position: relative; 307 | z-index: 10; /* Places buttons over full width title */ 308 | font-size: 12px; 309 | line-height: 23px; 310 | color: #fff; 311 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .3); 312 | background-color: #1eb0e9; 313 | background-image: -webkit-linear-gradient(top, #1eb0e9 0, #0984c6 100%); 314 | background-image: linear-gradient(to bottom, #1eb0e9 0, #0984c6 100%); 315 | border: 1px solid #0e5895; 316 | box-shadow: 0 1px rgba(255, 255, 255, .25); 317 | -webkit-box-flex: 0; 318 | box-flex: 0; 319 | } 320 | 321 | 322 | /* Hacky way to right align buttons outside of flex-box system 323 | Note: is only absolutely positioned button, would be better if flex-box had an "align right" option */ 324 | .bar-title .title + [class*="button"]:last-child, 325 | .bar-title .button + [class*="button"]:last-child, 326 | .bar-title [class*="button"].pull-right { 327 | position: absolute; 328 | top: 5px; 329 | right: 5px; 330 | } 331 | 332 | /* Override standard button active states */ 333 | .bar-title .button:active { 334 | color: #fff; 335 | background-color: #0876b1; 336 | } 337 | 338 | /* Directional buttons in title bars (thanks to @GregorAdams for solution - http://cssnerd.com/2011/11/30/the-best-pure-css3-ios-style-arrow-back-button/) 339 | -------------------------------------------------- */ 340 | 341 | /* Add relative positioning so :before content is positioned properly */ 342 | .bar-title .button-prev, 343 | .bar-title .button-next { 344 | position: relative; 345 | } 346 | 347 | /* Prev/next button base styles */ 348 | .bar-title .button-prev { 349 | margin-left: 7px; /* Push over to make room for :before content */ 350 | border-left: 0; 351 | border-bottom-left-radius: 10px 15px; 352 | border-top-left-radius: 10px 15px; 353 | } 354 | .bar-title .button-next { 355 | margin-right: 7px; /* Push over to make room for :before content */ 356 | border-right: 0; 357 | border-top-right-radius: 10px 15px; 358 | border-bottom-right-radius: 10px 15px; 359 | } 360 | 361 | /* Pointed part of directional button */ 362 | .bar-title .button-prev:before, 363 | .bar-title .button-next:before { 364 | position: absolute; 365 | top: 2px; 366 | width: 27px; 367 | height: 27px; 368 | border-radius: 30px 100px 2px 40px / 2px 40px 30px 100px; 369 | content: ''; 370 | box-shadow: inset 1px 0 #0e5895, inset 0 1px #0e5895; 371 | -webkit-mask-image: -webkit-gradient(linear, left top, right bottom, from(#000), color-stop(.33, #000), color-stop(.5, transparent), to(transparent)); 372 | mask-image: gradient(linear, left top, right bottom, from(#000), color-stop(.33, #000), color-stop(.5, transparent), to(transparent)); 373 | } 374 | .bar-title .button-prev:before { 375 | left: -5px; 376 | background-image: -webkit-gradient(linear, left bottom, right top, from(#0984c6), to(#1eb0e9)); 377 | background-image: gradient(linear, left bottom, right top, from(#0984c6), to(#1eb0e9)); 378 | border-left: 1.5px solid rgba(255, 255, 255, .25); 379 | -webkit-transform: rotate(-45deg) skew(-10deg, -10deg); 380 | transform: rotate(-45deg) skew(-10deg, -10deg); 381 | } 382 | .bar-title .button-next:before { 383 | right: -5px; 384 | background-image: -webkit-gradient(linear, left bottom, right top, from(#1eb0e9), to(#0984c6)); 385 | background-image: gradient(linear, left bottom, right top, from(#1eb0e9), to(#0984c6)); 386 | border-top: 1.5px solid rgba(255, 255, 255, .25); 387 | -webkit-transform: rotate(135deg) skew(-10deg, -10deg); 388 | transform: rotate(135deg) skew(-10deg, -10deg); 389 | } 390 | 391 | /* Active states for the directional buttons */ 392 | .bar-title .button-prev:active, 393 | .bar-title .button-next:active, 394 | .bar-title .button-prev:active:before, 395 | .bar-title .button-next:active:before { 396 | color: #fff; 397 | background-color: #0876b1; 398 | background-image: none; 399 | } 400 | .bar-title .button-prev:active:before, 401 | .bar-title .button-next:active:before { 402 | content: ''; 403 | } 404 | .bar-title .button-prev:active:before { 405 | box-shadow: inset 0 3px 3px rgba(0, 0, 0, .2); 406 | } 407 | .bar-title .button-next:active:before { 408 | box-shadow: inset 0 -3px 3px rgba(0, 0, 0, .2); 409 | } 410 | 411 | /* Block buttons in any bar 412 | -------------------------------------------------- */ 413 | 414 | /* Add proper padding and replace buttons normal dropshadow with a shine from bar */ 415 | [class*="bar"] .button-block { 416 | padding: 7px 0; 417 | margin-bottom: 0; 418 | box-shadow: inset 0 1px 1px rgba(255, 255, 255, .4), 0 1px rgba(255, 255, 255, .8); 419 | } 420 | 421 | /* Override standard padding changes for .button-blocks */ 422 | [class*="bar"] .button-block:active { 423 | padding: 7px 0; 424 | } 425 | 426 | /* Segmented controller in any bar 427 | -------------------------------------------------- */ 428 | 429 | /* Remove standard segmented bottom margin */ 430 | [class*="bar-"] .segmented-controller { 431 | margin-bottom: 0; 432 | } 433 | 434 | /* Add margins between segmented controllers and buttons */ 435 | [class*="bar-"] .segmented-controller + [class*="button"], 436 | [class*="bar-"] [class*="button"] + .segmented-controller { 437 | margin-left: 5px; 438 | } 439 | 440 | /* Segmented controller in a title bar 441 | -------------------------------------------------- */ 442 | 443 | .bar-title .segmented-controller { 444 | line-height: 18px; 445 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); 446 | background-color: #1eb0e9; 447 | background-image: -webkit-linear-gradient(top, #1eb0e9 0, #0984c6 100%); 448 | background-image: linear-gradient(to bottom, #1eb0e9 0, #0984c6 100%); 449 | border: 1px solid #0e5895; 450 | border-radius: 3px; 451 | box-shadow: 0 1px rgba(255, 255, 255, .25); 452 | -webkit-box-flex: 1; 453 | box-flex: 1; 454 | } 455 | 456 | /* Set color for tab border and highlight */ 457 | .bar-title .segmented-controller li { 458 | border-left: 1px solid #0e5895; 459 | box-shadow: inset 1px 0 rgba(255, 255, 255, .25); 460 | } 461 | 462 | /* Remove inset shadow from first tab or one to the right of the active tab */ 463 | .bar-title .segmented-controller .active + li, 464 | .bar-title .segmented-controller li:first-child { 465 | box-shadow: none; 466 | } 467 | 468 | /* Remove left-hand border from first tab */ 469 | .bar-title .segmented-controller li:first-child { 470 | border-left-width: 0; 471 | } 472 | 473 | /* Depressed state (active) */ 474 | .bar-title .segmented-controller li.active { 475 | background-color: #0082c4; 476 | box-shadow: inset 0 1px 6px rgba(0, 0, 0, .3); 477 | } 478 | 479 | /* Set color of links to white */ 480 | .bar-title .segmented-controller li > a { 481 | color: #fff; 482 | } 483 | 484 | 485 | /* Search forms in standard bar 486 | -------------------------------------------------- */ 487 | 488 | /* Position/size search bar within the bar */ 489 | .bar-standard input[type=search] { 490 | height: 32px; 491 | margin: 0; 492 | }/* Lists 493 | -------------------------------------------------- */ 494 | 495 | /* Remove usual bullet styles from list */ 496 | .list { 497 | margin-bottom: 10px; 498 | list-style: none; 499 | background-color: #fff; 500 | } 501 | 502 | /* Pad each list item and add dividers */ 503 | .list li { 504 | position: relative; 505 | padding: 20px 60px 20px 10px; /* Given extra right padding to accomodate counts, chevrons or buttons */ 506 | border-bottom: 1px solid rgba(0, 0, 0, .1); 507 | } 508 | 509 | /* Give top border to first list items */ 510 | .list li:first-child { 511 | border-top: 1px solid rgba(0, 0, 0, .1); 512 | } 513 | 514 | /* If a list of links, make sure the child takes up full list item tap area (want to avoid selecting child buttons though) */ 515 | .list li > a:not([class*="button"]) { 516 | position: relative; 517 | display: block; 518 | padding: inherit; 519 | margin: -20px -60px -20px -10px; 520 | color: inherit; 521 | } 522 | 523 | /* Inset list 524 | -------------------------------------------------- */ 525 | 526 | .list.inset { 527 | width: auto; 528 | margin-right: 10px; 529 | margin-left: 10px; 530 | border: 1px solid rgba(0, 0, 0, .1); 531 | border-radius: 6px; 532 | box-sizing: border-box; 533 | } 534 | 535 | /* Remove border from first/last standard list items to avoid double border at top/bottom of lists */ 536 | .list.inset li:first-child { 537 | border-top-width: 0; 538 | } 539 | .list.inset li:last-child { 540 | border-bottom-width: 0; 541 | } 542 | 543 | 544 | /* List dividers 545 | -------------------------------------------------- */ 546 | 547 | .list .list-divider { 548 | position: relative; 549 | top: -1px; 550 | padding-top: 6px; 551 | padding-bottom: 6px; 552 | font-size: 12px; 553 | font-weight: bold; 554 | line-height: 18px; 555 | text-shadow: 0 1px 0 rgba(255, 255, 255, .5); 556 | background-color: #f8f8f8; 557 | background-image: -webkit-linear-gradient(top, #f8f8f8 0, #eee 100%); 558 | background-image: linear-gradient(to bottom, #f8f8f8 0, #eee 100%); 559 | border-top: 1px solid rgba(0, 0, 0, .1); 560 | border-bottom: 1px solid rgba(0, 0, 0, .1); 561 | box-shadow: inset 0 1px 1px rgba(255, 255, 255, .4); 562 | } 563 | 564 | /* Rounding first divider on inset lists and remove border on the top */ 565 | .list.inset .list-divider:first-child { 566 | top: 0; 567 | border-top-width: 0; 568 | border-radius: 6px 6px 0 0; 569 | } 570 | 571 | /* Rounding last divider on inset lists */ 572 | .list.inset .list-divider:last-child { 573 | border-radius: 0 0 6px 6px; 574 | } 575 | 576 | /* Right-aligned subcontent in lists (chevrons, buttons, counts and toggles) 577 | -------------------------------------------------- */ 578 | .list .chevron, 579 | .list [class*="button"], 580 | .list [class*="count"], 581 | .list .toggle { 582 | position: absolute; 583 | top: 50%; 584 | right: 10px; 585 | } 586 | 587 | /* Position chevrons/counts vertically centered on the right in list items */ 588 | .list .chevron, 589 | .list [class*="count"] { 590 | margin-top: -10px; /* Half height of chevron */ 591 | } 592 | 593 | /* Push count over if there's a sibling chevron */ 594 | .list .chevron + [class*="count"] { 595 | right: 30px; 596 | } 597 | 598 | /* Position buttons vertically centered on the right in list items */ 599 | .list [class*="button"] { 600 | left: auto; 601 | margin-top: -14px; /* Half height of button */ 602 | } 603 | 604 | .list .toggle { 605 | margin-top: -15px; /* Half height of toggle */ 606 | }/* Forms 607 | -------------------------------------------------- */ 608 | 609 | /* Force form elements to inherit font styles */ 610 | input, 611 | textarea, 612 | button, 613 | select { 614 | font-family: inherit; 615 | font-size: inherit; 616 | } 617 | 618 | /* Stretch inputs/textareas to full width and add height to maintain a consistent baseline */ 619 | select, 620 | textarea, 621 | input[type="text"], 622 | input[type=search], 623 | input[type="password"], 624 | input[type="datetime"], 625 | input[type="datetime-local"], 626 | input[type="date"], 627 | input[type="month"], 628 | input[type="time"], 629 | input[type="week"], 630 | input[type="number"], 631 | input[type="email"], 632 | input[type="url"], 633 | input[type="tel"], 634 | input[type="color"], 635 | .input-group { 636 | width: 100%; 637 | height: 40px; 638 | padding: 10px; 639 | margin-bottom: 10px; 640 | background-color: #fff; 641 | border: 1px solid rgba(0, 0, 0, .2); 642 | border-radius: 3px; 643 | box-shadow: 0 1px 1px rgba(255, 255, 255, .2), inset 0 1px 1px rgba(0, 0, 0, .1); 644 | -webkit-appearance: none; 645 | box-sizing: border-box; 646 | } 647 | 648 | /* Fully round search input */ 649 | input[type=search] { 650 | height: 34px; 651 | font-size: 14px; 652 | border-radius: 30px; 653 | } 654 | 655 | /* Allow text area's height to grow larger than a normal input */ 656 | textarea { 657 | height: auto; 658 | } 659 | 660 | /* Style select button to look like part of the Ratchet's style */ 661 | select { 662 | height: auto; 663 | font-size: 14px; 664 | background-color: #f8f8f8; 665 | background-image: -webkit-linear-gradient(top, #f8f8f8 0%, #d4d4d4 100%); 666 | background-image: linear-gradient(to bottom, #f8f8f8 0%, #d4d4d4 100%); 667 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .1); 668 | } 669 | 670 | 671 | /* Input groups (cluster multiple inputs together into a single group) 672 | -------------------------------------------------- */ 673 | 674 | /* Reset from initial form setup styles */ 675 | .input-group { 676 | width: auto; 677 | height: auto; 678 | padding: 0; 679 | } 680 | 681 | /* Remove spacing, borders, shadows and rounding since it all belongs on the .input-group not the input */ 682 | .input-group input { 683 | margin-bottom: 0; 684 | background-color: transparent; 685 | border: 0; 686 | border-bottom: 1px solid rgba(0, 0, 0, .2); 687 | border-radius: 0; 688 | box-shadow: none; 689 | } 690 | 691 | /* Remove bottom border on last input to avoid double bottom border */ 692 | .input-group input:last-child { 693 | border-bottom-width: 0; 694 | } 695 | 696 | /* Input groups with labels 697 | -------------------------------------------------- */ 698 | 699 | /* To use labels with input groups, wrap a label and an input in an .input-row */ 700 | .input-row { 701 | overflow: hidden; 702 | border-bottom: 1px solid rgba(0, 0, 0, .2); 703 | } 704 | 705 | /* Remove bottom border on last input-row to avoid double bottom border */ 706 | .input-row:last-child { 707 | border-bottom-width: 0; 708 | } 709 | 710 | /* Labels get floated left with a set percentage width */ 711 | .input-row label { 712 | float: left; 713 | width: 25%; 714 | padding: 11px 10px 9px 13px; /* Optimizing the baseline for mobile. */ 715 | font-weight: bold; 716 | } 717 | 718 | /* Actual inputs float to right of labels and also have a set percentage */ 719 | .input-row label + input { 720 | float: right; 721 | width: 65%; 722 | padding-left: 0; 723 | margin-bottom: 0; 724 | border-bottom: 0; 725 | }/* General button styles 726 | -------------------------------------------------- */ 727 | 728 | [class*="button"] { 729 | position: relative; 730 | display: inline-block; 731 | padding: 4px 12px; 732 | margin: 0; 733 | font-weight: bold; 734 | line-height: 18px; 735 | color: #333; 736 | text-align: center; 737 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 738 | vertical-align: top; 739 | cursor: pointer; 740 | background-color: #f8f8f8; 741 | background-image: -webkit-linear-gradient(top, #f8f8f8 0, #d4d4d4 100%); 742 | background-image: linear-gradient(to bottom, #f8f8f8 0, #d4d4d4 100%); 743 | border: 1px solid rgba(0, 0, 0, .3); 744 | border-radius: 3px; 745 | box-shadow: inset 0 1px 1px rgba(255, 255, 255, .4), 0 1px 2px rgba(0, 0, 0, .05); 746 | } 747 | 748 | /* Active */ 749 | [class*="button"]:active { 750 | padding-top: 5px; 751 | padding-bottom: 3px; 752 | color: #333; 753 | background-color: #ccc; 754 | background-image: none; 755 | box-shadow: inset 0 3px 3px rgba(0, 0, 0, .2); 756 | } 757 | 758 | /* Button modifiers 759 | -------------------------------------------------- */ 760 | 761 | /* Overriding styles for buttons with modifiers */ 762 | .button-main, 763 | .button-positive, 764 | .button-negative { 765 | color: #fff; 766 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .3); 767 | } 768 | 769 | /* Main button */ 770 | .button-main { 771 | background-color: #1eafe7; 772 | background-image: -webkit-linear-gradient(top, #1eafe7 0, #1a97c8 100%); 773 | background-image: linear-gradient(to bottom, #1eafe7 0, #1a97c8 100%); 774 | border: 1px solid #117aaa; 775 | } 776 | 777 | /* Positive button */ 778 | .button-positive { 779 | background-color: #34ba15; 780 | background-image: -webkit-linear-gradient(top, #34ba15 0, #2da012 100%); 781 | background-image: linear-gradient(to bottom, #34ba15 0, #2da012 100%); 782 | border: 1px solid #278f0f; 783 | } 784 | 785 | /* Negative button */ 786 | .button-negative { 787 | background-color: #e71e1e; 788 | background-image: -webkit-linear-gradient(top, #e71e1e 0,#c71a1a 100%); 789 | background-image: linear-gradient(to bottom, #e71e1e 0, #c71a1a 100%); 790 | border: 1px solid #b51a1a; 791 | } 792 | 793 | /* Active state for buttons with modifiers */ 794 | .button-main:active, 795 | .button-positive:active, 796 | .button-negative:active { 797 | color: #fff; 798 | } 799 | .button-main:active { 800 | background-color: #0876b1; 801 | } 802 | .button-positive:active { 803 | background-color: #298f11; 804 | } 805 | .button-negative:active { 806 | background-color: #b21a1a; 807 | } 808 | 809 | /* Block level buttons (full width buttons) */ 810 | .button-block { 811 | display: block; 812 | padding: 11px 0 13px; 813 | margin-bottom: 10px; 814 | font-size: 16px; 815 | } 816 | 817 | /* Active state for block level buttons */ 818 | .button-block:active { 819 | padding: 12px 0; 820 | } 821 | 822 | /* Counts in buttons 823 | -------------------------------------------------- */ 824 | 825 | /* Generic styles for all counts within buttons */ 826 | [class*="button"] [class*="count"] { 827 | padding-top: 2px; 828 | padding-bottom: 2px; 829 | margin-right: -4px; 830 | margin-left: 4px; 831 | text-shadow: none; 832 | background-color: rgba(0, 0, 0, .2); 833 | box-shadow: inset 0 1px 1px -1px #000000, 0 1px 1px -1px #fff; 834 | } 835 | 836 | /* Position counts within block level buttons 837 | Note: These are absolutely positioned so that text of button isn't "pushed" by count and always 838 | stays at true center of button */ 839 | .button-block [class*="count"] { 840 | position: absolute; 841 | right: 0; 842 | padding-top: 4px; 843 | padding-bottom: 4px; 844 | margin-right: 10px; 845 | }/* Chevrons 846 | -------------------------------------------------- */ 847 | 848 | .chevron { 849 | display: block; 850 | height: 20px; 851 | } 852 | 853 | /* Base styles for both 1/2's of the chevron */ 854 | .chevron:before, 855 | .chevron:after { 856 | position: relative; 857 | display: block; 858 | width: 12px; 859 | height: 4px; 860 | background-color: #999; 861 | content: ''; 862 | } 863 | 864 | /* Position and rotate respective 1/2's of the chevron */ 865 | .chevron:before { 866 | top: 5px; 867 | -webkit-transform: rotate(45deg); 868 | transform: rotate(45deg); 869 | } 870 | .chevron:after { 871 | top: 7px; 872 | -webkit-transform: rotate(-45deg); 873 | transform: rotate(-45deg); 874 | }/* General count styles 875 | -------------------------------------------------- */ 876 | 877 | [class*="count"] { 878 | display: inline-block; 879 | padding: 4px 9px; 880 | font-size: 12px; 881 | font-weight: bold; 882 | line-height: 13px; 883 | color: #fff; 884 | background-color: rgba(0, 0, 0, .3); 885 | border-radius: 100px; 886 | } 887 | 888 | /* Count modifiers 889 | -------------------------------------------------- */ 890 | 891 | /* Overriding styles for counts with modifiers */ 892 | .count-main, 893 | .count-positive, 894 | .count-negative { 895 | color: #fff; 896 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .3); 897 | } 898 | 899 | /* Main count */ 900 | .count-main { 901 | background-color: #1eafe7; 902 | background-image: -webkit-linear-gradient(top, #1eafe7 0, #1a97c8 100%); 903 | background-image: linear-gradient(to bottom, #1eafe7 0, #1a97c8 100%); 904 | } 905 | 906 | /* Positive count */ 907 | .count-positive { 908 | background-color: #34ba15; 909 | background-image: -webkit-linear-gradient(top, #34ba15 0, #2da012 100%); 910 | background-image: linear-gradient(to bottom, #34ba15 0, #2da012 100%); 911 | } 912 | 913 | /* Negative count */ 914 | .count-negative { 915 | background-color: #e71e1e; 916 | background-image: -webkit-linear-gradient(top, #e71e1e 0,#c71a1a 100%); 917 | background-image: linear-gradient(to bottom, #e71e1e 0, #c71a1a 100%); 918 | }/* Segmented controllers 919 | -------------------------------------------------- */ 920 | 921 | .segmented-controller { 922 | display: -webkit-box; 923 | display: box; 924 | margin-bottom: 10px; 925 | overflow: hidden; 926 | font-size: 12px; 927 | font-weight: bold; 928 | text-shadow: 0 1px rgba(255, 255, 255, .5); 929 | list-style: none; 930 | background-color: #f8f8f8; 931 | background-image: -webkit-linear-gradient(top, #f8f8f8 0, #d4d4d4 100%); 932 | background-image: linear-gradient(to bottom, #f8f8f8 0, #d4d4d4 100%); 933 | border: 1px solid #aaa; 934 | border-radius: 3px; 935 | box-shadow: inset 0 1px rgba(255, 255, 255, 0.5), 0 1px rgba(255, 255, 255, .8); 936 | -webkit-box-orient: horizontal; 937 | box-orient: horizontal; 938 | } 939 | 940 | /* Section within controller */ 941 | .segmented-controller li { 942 | overflow: hidden; 943 | text-align: center; 944 | white-space: nowrap; 945 | border-left: 1px solid #aaa; 946 | box-shadow: inset 1px 0 rgba(255, 255, 255, .5); 947 | -webkit-box-flex: 1; 948 | box-flex: 1; 949 | } 950 | 951 | /* Link that fills each section */ 952 | .segmented-controller li > a { 953 | display: block; 954 | padding: 8px 16px; 955 | overflow: hidden; 956 | line-height: 15px; 957 | color: #333; 958 | text-overflow: ellipsis; 959 | } 960 | 961 | /* Remove border-left and shadow from first section */ 962 | .segmented-controller li:first-child { 963 | border-left-width: 0; 964 | box-shadow: none; 965 | } 966 | 967 | /* Active segment of controller */ 968 | .segmented-controller li.active { 969 | background-color: #ccc; 970 | box-shadow: inset 0 1px 5px rgba(0, 0, 0, .3); 971 | } 972 | 973 | .segmented-controller-item { 974 | display: none; 975 | } 976 | 977 | .segmented-controller-item.active { 978 | display: block; 979 | }/* Popovers (to be used with popovers.js) 980 | -------------------------------------------------- */ 981 | 982 | .popover { 983 | position: fixed; 984 | top: 55px; 985 | left: 50%; 986 | z-index: 20; 987 | display: none; 988 | width: 280px; 989 | padding: 5px; 990 | margin-left: -146px; 991 | background-color: #555; 992 | background-image: -webkit-linear-gradient(top, #555 5%, #555 6%, #111 30%); 993 | background-image: linear-gradient(to bottom, #555 5%, #555 6%,#111 30%); 994 | border: 1px solid #111; 995 | border-radius: 6px; 996 | opacity: 0; 997 | box-shadow: inset 0 1px 1px -1px #fff, 0 3px 10px rgba(0, 0, 0, .3); 998 | -webkit-transform: translate3d(0, -15px, 0); 999 | transform: translate3d(0, -15px, 0); 1000 | -webkit-transition: -webkit-transform 0.2s ease-in-out, opacity 0.2s ease-in-out; 1001 | transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out; 1002 | } 1003 | 1004 | /* Caret on top of popover using CSS triangles (thanks to @chriscoyier for solution) */ 1005 | .popover:before, 1006 | .popover:after { 1007 | position: absolute; 1008 | left: 50%; 1009 | width: 0; 1010 | height: 0; 1011 | content: ''; 1012 | } 1013 | .popover:before { 1014 | top: -20px; 1015 | margin-left: -21px; 1016 | border-right: 21px solid transparent; 1017 | border-bottom: 21px solid #111; 1018 | border-left: 21px solid transparent; 1019 | } 1020 | .popover:after { 1021 | top: -19px; 1022 | margin-left: -20px; 1023 | border-right: 20px solid transparent; 1024 | border-bottom: 20px solid #555; 1025 | border-left: 20px solid transparent; 1026 | } 1027 | 1028 | /* Wrapper for a title and buttons */ 1029 | .popover-header { 1030 | display: -webkit-box; 1031 | display: box; 1032 | height: 34px; 1033 | margin-bottom: 5px; 1034 | } 1035 | 1036 | /* Centered title for popover */ 1037 | .popover-header .title { 1038 | position: absolute; 1039 | top: 0; 1040 | left: 0; 1041 | width: 100%; 1042 | margin: 15px 0; 1043 | font-size: 16px; 1044 | font-weight: bold; 1045 | line-height: 12px; 1046 | color: #fff; 1047 | text-align: center; 1048 | text-shadow: 0 -1px rgba(0, 0, 0, .5); 1049 | } 1050 | 1051 | /* Generic style for all buttons in .popover-header */ 1052 | .popover-header [class*="button"] { 1053 | z-index: 25; 1054 | font-size: 12px; 1055 | line-height: 22px; 1056 | color: #fff; 1057 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .3); 1058 | background-color: #454545; 1059 | background-image: -webkit-linear-gradient(top, #454545 0, #353535 100%); 1060 | background-image: linear-gradient(to bottom, #454545 0, #353535 100%); 1061 | border: 1px solid #111; 1062 | -webkit-box-flex: 0; 1063 | box-flex: 0; 1064 | } 1065 | 1066 | /* Hacky way to right align buttons outside of flex-box system 1067 | Note: is only absolutely positioned button, would be better if flex-box had an "align right" option */ 1068 | .popover-header .title + [class*="button"]:last-child, 1069 | .popover-header .button + [class*="button"]:last-child, 1070 | .popover-header [class*="button"].pull-right { 1071 | position: absolute; 1072 | top: 5px; 1073 | right: 5px; 1074 | } 1075 | 1076 | /* Active state for popover header buttons */ 1077 | .popover-header .button:active { 1078 | color: #fff; 1079 | background-color: #0876b1; 1080 | } 1081 | 1082 | /* Popover animation 1083 | -------------------------------------------------- */ 1084 | 1085 | .popover.visible { 1086 | opacity: 1; 1087 | -webkit-transform: translate3d(0, 0, 0); 1088 | transform: translate3d(0, 0, 0); 1089 | } 1090 | 1091 | /* Backdrop (used as invisible touch escape) 1092 | -------------------------------------------------- */ 1093 | 1094 | .backdrop { 1095 | position: fixed; 1096 | top: 0; 1097 | right: 0; 1098 | bottom: 0; 1099 | left: 0; 1100 | z-index: 10; 1101 | } 1102 | 1103 | /* Block level buttons in popovers 1104 | -------------------------------------------------- */ 1105 | 1106 | /* Positioning and giving darker border to look sharp against dark popover */ 1107 | .popover .button-block { 1108 | margin-bottom: 5px; 1109 | border: 1px solid #111; 1110 | } 1111 | 1112 | /* Remove extra margin on bottom of last button */ 1113 | .popover .button-block:last-child { 1114 | margin-bottom: 0; 1115 | } 1116 | 1117 | /* Lists in popovers 1118 | -------------------------------------------------- */ 1119 | 1120 | .popover .list { 1121 | width: auto; 1122 | max-height: 250px; 1123 | margin-right: 0; 1124 | margin-bottom: 0; 1125 | margin-left: 0; 1126 | overflow: auto; 1127 | background-color: #fff; 1128 | border: 1px solid #000; 1129 | border-radius: 3px; 1130 | -webkit-overflow-scrolling: touch; 1131 | }/* Slider styles (to be used with sliders.js) 1132 | -------------------------------------------------- */ 1133 | 1134 | /* Width/height of slider */ 1135 | .slider, 1136 | .slider > li { 1137 | width: 100%; 1138 | height: 200px; 1139 | } 1140 | 1141 | /* Outer wrapper for slider */ 1142 | .slider { 1143 | overflow: hidden; 1144 | background-color: #000; 1145 | } 1146 | 1147 | /* Inner wrapper for slider (width of all slides together) */ 1148 | .slider > ul { 1149 | position: relative; 1150 | font-size: 0; /* Remove spaces from inline-block children */ 1151 | white-space: nowrap; 1152 | -webkit-transition: all 0 linear; 1153 | transition: all 0 linear; 1154 | } 1155 | 1156 | /* Individual slide */ 1157 | .slider > ul > li { 1158 | display: inline-block; 1159 | vertical-align: top; /* Ensure that li always aligns to top */ 1160 | width: 100%; 1161 | height: 100%; 1162 | } 1163 | 1164 | /* Required reset of font-size to same as standard body */ 1165 | .slider > ul > li > * { 1166 | font-size: 14px; 1167 | }/* Toggle styles (to be used with toggles.js) 1168 | -------------------------------------------------- */ 1169 | 1170 | .toggle { 1171 | position: relative; 1172 | width: 75px; 1173 | height: 28px; 1174 | background-color: #eee; 1175 | border: 1px solid #bbb; 1176 | border-radius: 20px; 1177 | box-shadow: inset 0 0 4px rgba(0, 0, 0, .1); 1178 | } 1179 | 1180 | /* Text indicating "on" or "off". Default is "off" */ 1181 | .toggle:before { 1182 | position: absolute; 1183 | right: 13px; 1184 | font-weight: bold; 1185 | line-height: 28px; 1186 | color: #777; 1187 | text-shadow: 0 1px #fff; 1188 | text-transform: uppercase; 1189 | content: "Off"; 1190 | } 1191 | 1192 | /* Sliding handle */ 1193 | .toggle-handle { 1194 | position: absolute; 1195 | top: -1px; 1196 | left: -1px; 1197 | z-index: 2; 1198 | width: 28px; 1199 | height: 28px; 1200 | background-color: #fff; 1201 | background-image: -webkit-linear-gradient(top, #fff 0, #f2f2f2 100%); 1202 | background-image: linear-gradient(to bottom, #fff 0, #f2f2f2 100%); 1203 | border: 1px solid rgba(0, 0, 0, .2); 1204 | border-radius: 100px; 1205 | -webkit-transition: -webkit-transform 0.1s ease-in-out, border 0.1s ease-in-out; 1206 | transition: transform 0.1s ease-in-out, border 0.1s ease-in-out; 1207 | } 1208 | 1209 | /* Active state for toggle */ 1210 | .toggle.active { 1211 | background-color: #19a8e4; 1212 | background-image: -webkit-linear-gradient(top, #088cd4 0, #19a8e4 100%); 1213 | background-image: linear-gradient(to bottom, #088cd4 0, #19a8e4 100%); 1214 | border: 1px solid #096c9d; 1215 | box-shadow: inset 0 0 15px rgba(255, 255, 255, .25); 1216 | } 1217 | 1218 | /* Active state for toggle handle */ 1219 | .toggle.active .toggle-handle { 1220 | border-color: #0a76ad; 1221 | -webkit-transform: translate3d(48px,0,0); 1222 | transform: translate3d(48px,0,0); 1223 | } 1224 | 1225 | /* Change "off" to "on" for active state */ 1226 | .toggle.active:before { 1227 | right: auto; 1228 | left: 15px; 1229 | color: #fff; 1230 | text-shadow: 0 -1px rgba(0, 0, 0, 0.25); 1231 | content: "On"; 1232 | }/* Push styles (to be used with push.js) 1233 | -------------------------------------------------- */ 1234 | 1235 | /* Fade animation */ 1236 | .content.fade { 1237 | left: 0; 1238 | opacity: 0; 1239 | -webkit-transition: opacity .2s ease-in-out; 1240 | transition: opacity .2s ease-in-out; 1241 | } 1242 | .content.fade.in { 1243 | opacity: 1; 1244 | } 1245 | 1246 | /* Slide animation */ 1247 | .content.slide { 1248 | -webkit-transform: translate3d(0, 0, 0); 1249 | transform: translate3d(0, 0, 0); 1250 | -webkit-transition: -webkit-transform .25s ease-in-out; 1251 | transition: transform .25s ease-in-out; 1252 | } 1253 | .content.slide.left { 1254 | -webkit-transform: translate3d(-100%, 0, 0); 1255 | transform: translate3d(-100%, 0, 0); 1256 | } 1257 | .content.slide.right { 1258 | -webkit-transform: translate3d(100%, 0, 0); 1259 | transform: translate3d(100%, 0, 0); 1260 | } -------------------------------------------------------------------------------- /www/share-game.js: -------------------------------------------------------------------------------- 1 | var createClient = require('../') 2 | var highlight = require('voxel-highlight') 3 | var extend = require('extend') 4 | var voxelPlayer = require('voxel-player') 5 | var createGame = require('voxel-engine') 6 | var texturePath = require('painterly-textures')(__dirname) 7 | var texturePath = "textures/" 8 | var voxel = require('voxel') 9 | var game 10 | 11 | module.exports = function(opts, setup) { 12 | setup = setup || defaultSetup 13 | opts = extend({}, opts || {}) 14 | 15 | var settings = { 16 | //Sphere is on this client, Valley is default on server 17 | generate: voxel.generator['Sphere'], 18 | chunkDistance: 2, 19 | materials: [ 20 | ['grass', 'dirt', 'grass_dirt'], 21 | 'obsidian', 22 | 'brick', 23 | 'grass' 24 | ], 25 | texturePath: texturePath, 26 | worldOrigin: [0, 0, 0], 27 | controls: { discreteFire: true }, 28 | avatarInitialPosition: [2, 20, 2], 29 | resetSettings: true 30 | } 31 | 32 | var game = {} 33 | settings.generatorToString = settings.generate.toString() 34 | game.settings = settings 35 | var client = createClient(opts.server || "ws://localhost:8080/", game) 36 | 37 | client.emitter.on('noMoreChunks', function(id) { 38 | console.log("Attaching to the container and creating player") 39 | var container = opts.container || document.body 40 | game = client.game 41 | game.appendTo(container) 42 | if (game.notCapable()) return game 43 | var createPlayer = voxelPlayer(game) 44 | 45 | // create the player from a minecraft skin file and tell the 46 | // game to use it as the main player 47 | var avatar = createPlayer('viking.png') 48 | window.avatar = avatar 49 | avatar.possess() 50 | avatar.position.set(2, 20, 4) 51 | 52 | setup(game, avatar, client) 53 | }) 54 | 55 | return game 56 | } 57 | 58 | function defaultSetup(game, avatar, client) { 59 | // highlight blocks when you look at them, hold for block placement 60 | var blockPosPlace, blockPosErase 61 | var hl = game.highlighter = highlight(game, { color: 0xff0000 }) 62 | hl.on('highlight', function (voxelPos) { blockPosErase = voxelPos }) 63 | hl.on('remove', function (voxelPos) { blockPosErase = null }) 64 | hl.on('highlight-adjacent', function (voxelPos) { blockPosPlace = voxelPos }) 65 | hl.on('remove-adjacent', function (voxelPos) { blockPosPlace = null }) 66 | 67 | // toggle between first and third person modes 68 | window.addEventListener('keydown', function (ev) { 69 | if (ev.keyCode === 'R'.charCodeAt(0)) avatar.toggle() 70 | }) 71 | 72 | // block interaction stuff, uses highlight data 73 | var currentMaterial = 1 74 | 75 | game.on('fire', function (target, state) { 76 | var position = blockPosPlace 77 | if (position) { 78 | game.createBlock(position, currentMaterial) 79 | client.emitter.emit('set', position, currentMaterial) 80 | } else { 81 | position = blockPosErase 82 | if (position) { 83 | game.setBlock(position, 0) 84 | console.log("Erasing point at " + JSON.stringify(position)) 85 | client.emitter.emit('set', position, 0) 86 | } 87 | } 88 | }) 89 | } -------------------------------------------------------------------------------- /www/share.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Voxels 5 | 6 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |
46 |
    47 |
    48 |
    49 |
    50 |
    51 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /www/share.js: -------------------------------------------------------------------------------- 1 | require('./share-game.js')({server: "ws://localhost:8080/"}) -------------------------------------------------------------------------------- /www/textures/brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/textures/brick.png -------------------------------------------------------------------------------- /www/textures/crate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/textures/crate.png -------------------------------------------------------------------------------- /www/textures/dirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/textures/dirt.png -------------------------------------------------------------------------------- /www/textures/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/textures/grass.png -------------------------------------------------------------------------------- /www/textures/grass_dirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/textures/grass_dirt.png -------------------------------------------------------------------------------- /www/textures/obsidian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/textures/obsidian.png -------------------------------------------------------------------------------- /www/textures/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/textures/snow.png -------------------------------------------------------------------------------- /www/textures/whitewool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/textures/whitewool.png -------------------------------------------------------------------------------- /www/viking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/voxel-client/9f9d2df9bb673a3e557caee4e50095ccf30c7938/www/viking.png --------------------------------------------------------------------------------