├── LICENSE ├── README.md ├── Rakefile ├── common.css ├── common.js ├── images ├── background.js ├── background.png ├── background │ ├── background.svg │ ├── hills.png │ ├── sky.png │ └── trees.png ├── mute.png ├── sprites.js ├── sprites.png └── sprites │ ├── billboard01.png │ ├── billboard02.png │ ├── billboard03.png │ ├── billboard04.png │ ├── billboard05.png │ ├── billboard06.png │ ├── billboard07.png │ ├── billboard08.png │ ├── billboard09.png │ ├── boulder1.png │ ├── boulder2.png │ ├── boulder3.png │ ├── bush1.png │ ├── bush2.png │ ├── cactus.png │ ├── car01.png │ ├── car02.png │ ├── car03.png │ ├── car04.png │ ├── column.png │ ├── dead_tree1.png │ ├── dead_tree2.png │ ├── palm_tree.png │ ├── player_left.png │ ├── player_right.png │ ├── player_straight.png │ ├── player_uphill_left.png │ ├── player_uphill_right.png │ ├── player_uphill_straight.png │ ├── semi.png │ ├── stump.png │ ├── tree1.png │ ├── tree2.png │ └── truck.png ├── index.html ├── music ├── racer.mp3 └── racer.ogg ├── stats.js ├── v1.straight.html ├── v2.curves.html ├── v3.hills.html └── v4.final.html /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | =============================================================================== 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Javascript Pseudo 3D Racer 2 | ========================== 3 | 4 | An Outrun-style pseudo-3d racing game in HTML5 and Javascript 5 | 6 | * [play the game](https://jakesgordon.com/games/racer/) 7 | * view the [source](https://github.com/jakesgordon/javascript-racer) 8 | * read about [how it works](https://jakesgordon.com/writing/javascript-racer/) 9 | 10 | Incrementally built up in 4 parts: 11 | 12 | * play the [straight road demo](https://jakesgordon.com/games/racer/v1-straight/) 13 | * play the [curves demo](https://jakesgordon.com/games/racer/v2-curves/) 14 | * play the [hills demo](https://jakesgordon.com/games/racer/v3-hills/) 15 | * play the [final version](https://jakesgordon.com/games/racer/) 16 | 17 | With detailed descriptions of how each part works: 18 | 19 | * read more about [v1 - straight roads](https://jakesgordon.com/writing/javascript-racer-v1-straight/) 20 | * read more about [v2 - curves](https://jakesgordon.com/writing/javascript-racer-v2-curves/) 21 | * read more about [v3 - hills](https://jakesgordon.com/writing/javascript-racer-v3-hills/) 22 | * read more about v4 - final (coming soon) 23 | 24 | A note on performance 25 | ===================== 26 | 27 | The performance of this game is **very** machine/browser dependent. It works quite well in modern 28 | browsers, especially those with GPU canvas acceleration, but a bad graphics driver can kill it stone 29 | dead. So your mileage may vary. There are controls provided to change the rendering resolution 30 | and the draw distance to scale to fit your machine. 31 | 32 | Currently supported browsers include: 33 | 34 | * Firefox (v12+) works great, 60fps at high res - Nice! 35 | * Chrome (v19+) works great, 60fps at high res... provided you dont have a bad GPU driver 36 | * IE9 - ok, 30fps at medium res... not great, but at least it works 37 | 38 | The current state of mobile browser performance is pretty dismal. Dont expect this to be playable on 39 | any mobile device. 40 | 41 | >> _NOTE: I havent actually spent anytime optimizing for performance yet. So it might be possible to 42 | make it play well on older browsers, but that's not really what this project is about._ 43 | 44 | A note on code structure 45 | ======================== 46 | 47 | This project happens to be implemented in javascript (because its easy for prototyping) but 48 | is not intended to demonstrate javascript techniques or best practices. In fact, in order to 49 | keep it simple to understand it embeds the javascript for each example directly in the HTML 50 | page (horror!) and, even worse, uses global variables and functions (OMG!). 51 | 52 | If I was building a real game I would have much more structure and organization to the 53 | code, but since its just a racing game tech demo, I have elected to [KISS](http://en.wikipedia.org/wiki/KISS_principle). 54 | 55 | FUTURE 56 | ====== 57 | 58 | It's quite astounding what it takes to actually [finish](https://jakesgordon.com/writing/defining-finished/) 59 | a game, even a simple one. And this is not a project that I plan on polishing into a finished state. It should 60 | really be considered just how to get started with a pseudo-3d racing game. 61 | 62 | If we were to try to turn it into a real game we would have to consider: 63 | 64 | * car sound fx 65 | * better synchronized music 66 | * full screen mode 67 | * HUD fx (flash on fastest lap, confetti, color coded speedometer, etc) 68 | * more accurate sprite collision 69 | * better car AI (steering, braking etc) 70 | * an actual crash when colliding at high speed 71 | * more bounce when car is off road 72 | * screen shake when off-road or collision 73 | * throw up dirt particles when off road 74 | * more dynamic camera (lower at faster speed, swoop over hills etc) 75 | * automatic resolution & drawDistance detection 76 | * projection based curves ? x,y rotation 77 | * sub-pixel aliasing artifacts on curves 78 | * smarter fog to cover sprites (blue against sky, cover sprites) 79 | * multiple stages, different maps 80 | * a lap map, with current position indicator 81 | * road splits and joins 82 | * day/night cycle 83 | * weather effects 84 | * tunnels, bridges, clouds, walls, buildings 85 | * city, desert, ocean 86 | * add city of seattle and space needle to background 87 | * 'bad guys' - add some competetor drivers to race against as well as the 'traffic' 88 | * different game modes - fastest lap, 1-on-1 racing, collect coins ? shoot bad guys ? 89 | * a whole lot of gameplay tuning 90 | * ... 91 | * ... 92 | 93 | Related Links 94 | ============= 95 | 96 | * [Lou's Pseudo-3d Page](http://www.extentofthejam.com/pseudo/) - high level how-to guide 97 | * [Racer 10k](https://github.com/onaluf/RacerJS) - another javascript racing game 98 | 99 | License 100 | ======= 101 | 102 | [MIT](http://en.wikipedia.org/wiki/MIT_License) license. 103 | 104 | >> NOTE: the music tracks included in this project are royalty free resources paid for and licensed 105 | from [Lucky Lion Studios](http://luckylionstudios.com/). They are licensed ONLY for use in this 106 | project and should not be reproduced. 107 | 108 | >> NOTE: the sprite graphics are placeholder graphics [borrowed](http://pixel.garoux.net/game/44) from the old 109 | genesis version of outrun and used here as teaching examples. If there are any pixel artists out there who want to 110 | provide original art to turn this into a real game please get in touch! 111 | 112 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | desc 'recreate sprite sheets' 3 | task 'resprite' do 4 | require 'sprite_factory' 5 | 6 | SpriteFactory.run!('images/sprites', :layout => :packed, :output_style => 'images/sprites.js', :margin => 5, :nocomments => true) do |images| 7 | SpriteHelper.javascript_style("SPRITES", images) 8 | end 9 | 10 | SpriteFactory.run!('images/background', :layout => :vertical, :output_style => 'images/background.js', :margin => 5, :nocomments => true) do |images| 11 | SpriteHelper.javascript_style("BACKGROUND", images) 12 | end 13 | 14 | end 15 | 16 | #------------------------------------------------------------------------------ 17 | 18 | module SpriteHelper 19 | 20 | # slightly unusual use of sprite-factory to generate a javascript object structure instead of CSS attributes... 21 | def self.javascript_style(variable, images) 22 | maxname = images.keys.inject(0) {|n,key| [n,key.length].max } 23 | rules = [] 24 | images.each do |name, i| 25 | name = name.upcase 26 | whitespace = ' '*(maxname-name.length) 27 | x = '%4d' % i[:cssx] 28 | y = '%4d' % i[:cssy] 29 | w = '%4d' % i[:cssw] 30 | h = '%4d' % i[:cssh] 31 | rules << " #{name}: #{whitespace}{ x: #{x}, y: #{y}, w: #{w}, h: #{h} }" 32 | end 33 | "var #{variable} = {\n#{rules.join(",\n")}\n};" 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /common.css: -------------------------------------------------------------------------------- 1 | 2 | /****************************************/ 3 | /* common styles used for v1 through v4 */ 4 | /****************************************/ 5 | 6 | body { font-family: Arial, Helvetica, sans-serif; } 7 | #stats { border: 2px solid black; } 8 | #controls { width: 28em; float: left; padding: 1em; font-size: 0.7em; } 9 | #controls th { text-align: right; vertical-align: middle; } 10 | #instructions { clear: left; float: left; width: 17em; padding: 1em; border: 1px solid black; box-shadow: 0 0 5px black; } 11 | #racer { position: relative; z-index: 0; width: 640px; height: 480px; margin-left: 20em; border: 2px solid black; } 12 | #canvas { position: absolute; z-index: 0; width: 640px; height: 480px; z-index: 0; background-color: #72D7EE; } 13 | #mute { background-position: 0px 0px; width: 32px; height: 32px; background: url(images/mute.png); display: inline-block; cursor: pointer; position: absolute; margin-left: 20em; } 14 | #mute.on { background-position: -32px 0px; } 15 | 16 | /**************************************************/ 17 | /* rudimentary heads up display (only used in v4) */ 18 | /**************************************************/ 19 | 20 | #hud { position: absolute; z-index: 1; width: 640px; padding: 5px 0; font-family: Verdana, Geneva, sans-serif; font-size: 0.8em; background-color: rgba(255,0,0,0.4); color: black; border-bottom: 2px solid black; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; } 21 | #hud .hud { background-color: rgba(255,255,255,0.6); padding: 5px; border: 1px solid black; margin: 0 5px; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; } 22 | #hud #speed { float: right; } 23 | #hud #current_lap_time { float: left; } 24 | #hud #last_lap_time { float: left; display: none; } 25 | #hud #fast_lap_time { display: block; width: 12em; margin: 0 auto; text-align: center; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; } 26 | #hud .value { color: black; font-weight: bold; } 27 | #hud .fastest { background-color: rgba(255,215,0,0.5); } 28 | 29 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | //========================================================================= 2 | // minimalist DOM helpers 3 | //========================================================================= 4 | 5 | var Dom = { 6 | 7 | get: function(id) { return ((id instanceof HTMLElement) || (id === document)) ? id : document.getElementById(id); }, 8 | set: function(id, html) { Dom.get(id).innerHTML = html; }, 9 | on: function(ele, type, fn, capture) { Dom.get(ele).addEventListener(type, fn, capture); }, 10 | un: function(ele, type, fn, capture) { Dom.get(ele).removeEventListener(type, fn, capture); }, 11 | show: function(ele, type) { Dom.get(ele).style.display = (type || 'block'); }, 12 | blur: function(ev) { ev.target.blur(); }, 13 | 14 | addClassName: function(ele, name) { Dom.toggleClassName(ele, name, true); }, 15 | removeClassName: function(ele, name) { Dom.toggleClassName(ele, name, false); }, 16 | toggleClassName: function(ele, name, on) { 17 | ele = Dom.get(ele); 18 | var classes = ele.className.split(' '); 19 | var n = classes.indexOf(name); 20 | on = (typeof on == 'undefined') ? (n < 0) : on; 21 | if (on && (n < 0)) 22 | classes.push(name); 23 | else if (!on && (n >= 0)) 24 | classes.splice(n, 1); 25 | ele.className = classes.join(' '); 26 | }, 27 | 28 | storage: window.localStorage || {} 29 | 30 | } 31 | 32 | //========================================================================= 33 | // general purpose helpers (mostly math) 34 | //========================================================================= 35 | 36 | var Util = { 37 | 38 | timestamp: function() { return new Date().getTime(); }, 39 | toInt: function(obj, def) { if (obj !== null) { var x = parseInt(obj, 10); if (!isNaN(x)) return x; } return Util.toInt(def, 0); }, 40 | toFloat: function(obj, def) { if (obj !== null) { var x = parseFloat(obj); if (!isNaN(x)) return x; } return Util.toFloat(def, 0.0); }, 41 | limit: function(value, min, max) { return Math.max(min, Math.min(value, max)); }, 42 | randomInt: function(min, max) { return Math.round(Util.interpolate(min, max, Math.random())); }, 43 | randomChoice: function(options) { return options[Util.randomInt(0, options.length-1)]; }, 44 | percentRemaining: function(n, total) { return (n%total)/total; }, 45 | accelerate: function(v, accel, dt) { return v + (accel * dt); }, 46 | interpolate: function(a,b,percent) { return a + (b-a)*percent }, 47 | easeIn: function(a,b,percent) { return a + (b-a)*Math.pow(percent,2); }, 48 | easeOut: function(a,b,percent) { return a + (b-a)*(1-Math.pow(1-percent,2)); }, 49 | easeInOut: function(a,b,percent) { return a + (b-a)*((-Math.cos(percent*Math.PI)/2) + 0.5); }, 50 | exponentialFog: function(distance, density) { return 1 / (Math.pow(Math.E, (distance * distance * density))); }, 51 | 52 | increase: function(start, increment, max) { // with looping 53 | var result = start + increment; 54 | while (result >= max) 55 | result -= max; 56 | while (result < 0) 57 | result += max; 58 | return result; 59 | }, 60 | 61 | project: function(p, cameraX, cameraY, cameraZ, cameraDepth, width, height, roadWidth) { 62 | p.camera.x = (p.world.x || 0) - cameraX; 63 | p.camera.y = (p.world.y || 0) - cameraY; 64 | p.camera.z = (p.world.z || 0) - cameraZ; 65 | p.screen.scale = cameraDepth/p.camera.z; 66 | p.screen.x = Math.round((width/2) + (p.screen.scale * p.camera.x * width/2)); 67 | p.screen.y = Math.round((height/2) - (p.screen.scale * p.camera.y * height/2)); 68 | p.screen.w = Math.round( (p.screen.scale * roadWidth * width/2)); 69 | }, 70 | 71 | overlap: function(x1, w1, x2, w2, percent) { 72 | var half = (percent || 1)/2; 73 | var min1 = x1 - (w1*half); 74 | var max1 = x1 + (w1*half); 75 | var min2 = x2 - (w2*half); 76 | var max2 = x2 + (w2*half); 77 | return ! ((max1 < min2) || (min1 > max2)); 78 | } 79 | 80 | } 81 | 82 | //========================================================================= 83 | // POLYFILL for requestAnimationFrame 84 | //========================================================================= 85 | 86 | if (!window.requestAnimationFrame) { // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 87 | window.requestAnimationFrame = window.webkitRequestAnimationFrame || 88 | window.mozRequestAnimationFrame || 89 | window.oRequestAnimationFrame || 90 | window.msRequestAnimationFrame || 91 | function(callback, element) { 92 | window.setTimeout(callback, 1000 / 60); 93 | } 94 | } 95 | 96 | //========================================================================= 97 | // GAME LOOP helpers 98 | //========================================================================= 99 | 100 | var Game = { // a modified version of the game loop from my previous boulderdash game - see https://jakesgordon.com/writing/javascript-boulderdash/#gameloop 101 | 102 | run: function(options) { 103 | 104 | Game.loadImages(options.images, function(images) { 105 | 106 | options.ready(images); // tell caller to initialize itself because images are loaded and we're ready to rumble 107 | 108 | Game.setKeyListener(options.keys); 109 | 110 | var canvas = options.canvas, // canvas render target is provided by caller 111 | update = options.update, // method to update game logic is provided by caller 112 | render = options.render, // method to render the game is provided by caller 113 | step = options.step, // fixed frame step (1/fps) is specified by caller 114 | stats = options.stats, // stats instance is provided by caller 115 | now = null, 116 | last = Util.timestamp(), 117 | dt = 0, 118 | gdt = 0; 119 | 120 | function frame() { 121 | now = Util.timestamp(); 122 | dt = Math.min(1, (now - last) / 1000); // using requestAnimationFrame have to be able to handle large delta's caused when it 'hibernates' in a background or non-visible tab 123 | gdt = gdt + dt; 124 | while (gdt > step) { 125 | gdt = gdt - step; 126 | update(step); 127 | } 128 | render(); 129 | stats.update(); 130 | last = now; 131 | requestAnimationFrame(frame, canvas); 132 | } 133 | frame(); // lets get this party started 134 | Game.playMusic(); 135 | }); 136 | }, 137 | 138 | //--------------------------------------------------------------------------- 139 | 140 | loadImages: function(names, callback) { // load multiple images and callback when ALL images have loaded 141 | var result = []; 142 | var count = names.length; 143 | 144 | var onload = function() { 145 | if (--count == 0) 146 | callback(result); 147 | }; 148 | 149 | for(var n = 0 ; n < names.length ; n++) { 150 | var name = names[n]; 151 | result[n] = document.createElement('img'); 152 | Dom.on(result[n], 'load', onload); 153 | result[n].src = "images/" + name + ".png"; 154 | } 155 | }, 156 | 157 | //--------------------------------------------------------------------------- 158 | 159 | setKeyListener: function(keys) { 160 | var onkey = function(keyCode, mode) { 161 | var n, k; 162 | for(n = 0 ; n < keys.length ; n++) { 163 | k = keys[n]; 164 | k.mode = k.mode || 'up'; 165 | if ((k.key == keyCode) || (k.keys && (k.keys.indexOf(keyCode) >= 0))) { 166 | if (k.mode == mode) { 167 | k.action.call(); 168 | } 169 | } 170 | } 171 | }; 172 | Dom.on(document, 'keydown', function(ev) { onkey(ev.keyCode, 'down'); } ); 173 | Dom.on(document, 'keyup', function(ev) { onkey(ev.keyCode, 'up'); } ); 174 | }, 175 | 176 | //--------------------------------------------------------------------------- 177 | 178 | stats: function(parentId, id) { // construct mr.doobs FPS counter - along with friendly good/bad/ok message box 179 | 180 | var result = new Stats(); 181 | result.domElement.id = id || 'stats'; 182 | Dom.get(parentId).appendChild(result.domElement); 183 | 184 | var msg = document.createElement('div'); 185 | msg.style.cssText = "border: 2px solid gray; padding: 5px; margin-top: 5px; text-align: left; font-size: 1.15em; text-align: right;"; 186 | msg.innerHTML = "Your canvas performance is "; 187 | Dom.get(parentId).appendChild(msg); 188 | 189 | var value = document.createElement('span'); 190 | value.innerHTML = "..."; 191 | msg.appendChild(value); 192 | 193 | setInterval(function() { 194 | var fps = result.current(); 195 | var ok = (fps > 50) ? 'good' : (fps < 30) ? 'bad' : 'ok'; 196 | var color = (fps > 50) ? 'green' : (fps < 30) ? 'red' : 'gray'; 197 | value.innerHTML = ok; 198 | value.style.color = color; 199 | msg.style.borderColor = color; 200 | }, 5000); 201 | return result; 202 | }, 203 | 204 | //--------------------------------------------------------------------------- 205 | 206 | playMusic: function() { 207 | var music = Dom.get('music'); 208 | music.loop = true; 209 | music.volume = 0.05; // shhhh! annoying music! 210 | music.muted = (Dom.storage.muted === "true"); 211 | music.play(); 212 | Dom.toggleClassName('mute', 'on', music.muted); 213 | Dom.on('mute', 'click', function() { 214 | Dom.storage.muted = music.muted = !music.muted; 215 | Dom.toggleClassName('mute', 'on', music.muted); 216 | }); 217 | } 218 | 219 | } 220 | 221 | //========================================================================= 222 | // canvas rendering helpers 223 | //========================================================================= 224 | 225 | var Render = { 226 | 227 | polygon: function(ctx, x1, y1, x2, y2, x3, y3, x4, y4, color) { 228 | ctx.fillStyle = color; 229 | ctx.beginPath(); 230 | ctx.moveTo(x1, y1); 231 | ctx.lineTo(x2, y2); 232 | ctx.lineTo(x3, y3); 233 | ctx.lineTo(x4, y4); 234 | ctx.closePath(); 235 | ctx.fill(); 236 | }, 237 | 238 | //--------------------------------------------------------------------------- 239 | 240 | segment: function(ctx, width, lanes, x1, y1, w1, x2, y2, w2, fog, color) { 241 | 242 | var r1 = Render.rumbleWidth(w1, lanes), 243 | r2 = Render.rumbleWidth(w2, lanes), 244 | l1 = Render.laneMarkerWidth(w1, lanes), 245 | l2 = Render.laneMarkerWidth(w2, lanes), 246 | lanew1, lanew2, lanex1, lanex2, lane; 247 | 248 | ctx.fillStyle = color.grass; 249 | ctx.fillRect(0, y2, width, y1 - y2); 250 | 251 | Render.polygon(ctx, x1-w1-r1, y1, x1-w1, y1, x2-w2, y2, x2-w2-r2, y2, color.rumble); 252 | Render.polygon(ctx, x1+w1+r1, y1, x1+w1, y1, x2+w2, y2, x2+w2+r2, y2, color.rumble); 253 | Render.polygon(ctx, x1-w1, y1, x1+w1, y1, x2+w2, y2, x2-w2, y2, color.road); 254 | 255 | if (color.lane) { 256 | lanew1 = w1*2/lanes; 257 | lanew2 = w2*2/lanes; 258 | lanex1 = x1 - w1 + lanew1; 259 | lanex2 = x2 - w2 + lanew2; 260 | for(lane = 1 ; lane < lanes ; lanex1 += lanew1, lanex2 += lanew2, lane++) 261 | Render.polygon(ctx, lanex1 - l1/2, y1, lanex1 + l1/2, y1, lanex2 + l2/2, y2, lanex2 - l2/2, y2, color.lane); 262 | } 263 | 264 | Render.fog(ctx, 0, y1, width, y2-y1, fog); 265 | }, 266 | 267 | //--------------------------------------------------------------------------- 268 | 269 | background: function(ctx, background, width, height, layer, rotation, offset) { 270 | 271 | rotation = rotation || 0; 272 | offset = offset || 0; 273 | 274 | var imageW = layer.w/2; 275 | var imageH = layer.h; 276 | 277 | var sourceX = layer.x + Math.floor(layer.w * rotation); 278 | var sourceY = layer.y 279 | var sourceW = Math.min(imageW, layer.x+layer.w-sourceX); 280 | var sourceH = imageH; 281 | 282 | var destX = 0; 283 | var destY = offset; 284 | var destW = Math.floor(width * (sourceW/imageW)); 285 | var destH = height; 286 | 287 | ctx.drawImage(background, sourceX, sourceY, sourceW, sourceH, destX, destY, destW, destH); 288 | if (sourceW < imageW) 289 | ctx.drawImage(background, layer.x, sourceY, imageW-sourceW, sourceH, destW-1, destY, width-destW, destH); 290 | }, 291 | 292 | //--------------------------------------------------------------------------- 293 | 294 | sprite: function(ctx, width, height, resolution, roadWidth, sprites, sprite, scale, destX, destY, offsetX, offsetY, clipY) { 295 | 296 | // scale for projection AND relative to roadWidth (for tweakUI) 297 | var destW = (sprite.w * scale * width/2) * (SPRITES.SCALE * roadWidth); 298 | var destH = (sprite.h * scale * width/2) * (SPRITES.SCALE * roadWidth); 299 | 300 | destX = destX + (destW * (offsetX || 0)); 301 | destY = destY + (destH * (offsetY || 0)); 302 | 303 | var clipH = clipY ? Math.max(0, destY+destH-clipY) : 0; 304 | if (clipH < destH) 305 | ctx.drawImage(sprites, sprite.x, sprite.y, sprite.w, sprite.h - (sprite.h*clipH/destH), destX, destY, destW, destH - clipH); 306 | 307 | }, 308 | 309 | //--------------------------------------------------------------------------- 310 | 311 | player: function(ctx, width, height, resolution, roadWidth, sprites, speedPercent, scale, destX, destY, steer, updown) { 312 | 313 | var bounce = (1.5 * Math.random() * speedPercent * resolution) * Util.randomChoice([-1,1]); 314 | var sprite; 315 | if (steer < 0) 316 | sprite = (updown > 0) ? SPRITES.PLAYER_UPHILL_LEFT : SPRITES.PLAYER_LEFT; 317 | else if (steer > 0) 318 | sprite = (updown > 0) ? SPRITES.PLAYER_UPHILL_RIGHT : SPRITES.PLAYER_RIGHT; 319 | else 320 | sprite = (updown > 0) ? SPRITES.PLAYER_UPHILL_STRAIGHT : SPRITES.PLAYER_STRAIGHT; 321 | 322 | Render.sprite(ctx, width, height, resolution, roadWidth, sprites, sprite, scale, destX, destY + bounce, -0.5, -1); 323 | }, 324 | 325 | //--------------------------------------------------------------------------- 326 | 327 | fog: function(ctx, x, y, width, height, fog) { 328 | if (fog < 1) { 329 | ctx.globalAlpha = (1-fog) 330 | ctx.fillStyle = COLORS.FOG; 331 | ctx.fillRect(x, y, width, height); 332 | ctx.globalAlpha = 1; 333 | } 334 | }, 335 | 336 | rumbleWidth: function(projectedRoadWidth, lanes) { return projectedRoadWidth/Math.max(6, 2*lanes); }, 337 | laneMarkerWidth: function(projectedRoadWidth, lanes) { return projectedRoadWidth/Math.max(32, 8*lanes); } 338 | 339 | } 340 | 341 | //============================================================================= 342 | // RACING GAME CONSTANTS 343 | //============================================================================= 344 | 345 | var KEY = { 346 | LEFT: 37, 347 | UP: 38, 348 | RIGHT: 39, 349 | DOWN: 40, 350 | A: 65, 351 | D: 68, 352 | S: 83, 353 | W: 87 354 | }; 355 | 356 | var COLORS = { 357 | SKY: '#72D7EE', 358 | TREE: '#005108', 359 | FOG: '#005108', 360 | LIGHT: { road: '#6B6B6B', grass: '#10AA10', rumble: '#555555', lane: '#CCCCCC' }, 361 | DARK: { road: '#696969', grass: '#009A00', rumble: '#BBBBBB' }, 362 | START: { road: 'white', grass: 'white', rumble: 'white' }, 363 | FINISH: { road: 'black', grass: 'black', rumble: 'black' } 364 | }; 365 | 366 | var BACKGROUND = { 367 | HILLS: { x: 5, y: 5, w: 1280, h: 480 }, 368 | SKY: { x: 5, y: 495, w: 1280, h: 480 }, 369 | TREES: { x: 5, y: 985, w: 1280, h: 480 } 370 | }; 371 | 372 | var SPRITES = { 373 | PALM_TREE: { x: 5, y: 5, w: 215, h: 540 }, 374 | BILLBOARD08: { x: 230, y: 5, w: 385, h: 265 }, 375 | TREE1: { x: 625, y: 5, w: 360, h: 360 }, 376 | DEAD_TREE1: { x: 5, y: 555, w: 135, h: 332 }, 377 | BILLBOARD09: { x: 150, y: 555, w: 328, h: 282 }, 378 | BOULDER3: { x: 230, y: 280, w: 320, h: 220 }, 379 | COLUMN: { x: 995, y: 5, w: 200, h: 315 }, 380 | BILLBOARD01: { x: 625, y: 375, w: 300, h: 170 }, 381 | BILLBOARD06: { x: 488, y: 555, w: 298, h: 190 }, 382 | BILLBOARD05: { x: 5, y: 897, w: 298, h: 190 }, 383 | BILLBOARD07: { x: 313, y: 897, w: 298, h: 190 }, 384 | BOULDER2: { x: 621, y: 897, w: 298, h: 140 }, 385 | TREE2: { x: 1205, y: 5, w: 282, h: 295 }, 386 | BILLBOARD04: { x: 1205, y: 310, w: 268, h: 170 }, 387 | DEAD_TREE2: { x: 1205, y: 490, w: 150, h: 260 }, 388 | BOULDER1: { x: 1205, y: 760, w: 168, h: 248 }, 389 | BUSH1: { x: 5, y: 1097, w: 240, h: 155 }, 390 | CACTUS: { x: 929, y: 897, w: 235, h: 118 }, 391 | BUSH2: { x: 255, y: 1097, w: 232, h: 152 }, 392 | BILLBOARD03: { x: 5, y: 1262, w: 230, h: 220 }, 393 | BILLBOARD02: { x: 245, y: 1262, w: 215, h: 220 }, 394 | STUMP: { x: 995, y: 330, w: 195, h: 140 }, 395 | SEMI: { x: 1365, y: 490, w: 122, h: 144 }, 396 | TRUCK: { x: 1365, y: 644, w: 100, h: 78 }, 397 | CAR03: { x: 1383, y: 760, w: 88, h: 55 }, 398 | CAR02: { x: 1383, y: 825, w: 80, h: 59 }, 399 | CAR04: { x: 1383, y: 894, w: 80, h: 57 }, 400 | CAR01: { x: 1205, y: 1018, w: 80, h: 56 }, 401 | PLAYER_UPHILL_LEFT: { x: 1383, y: 961, w: 80, h: 45 }, 402 | PLAYER_UPHILL_STRAIGHT: { x: 1295, y: 1018, w: 80, h: 45 }, 403 | PLAYER_UPHILL_RIGHT: { x: 1385, y: 1018, w: 80, h: 45 }, 404 | PLAYER_LEFT: { x: 995, y: 480, w: 80, h: 41 }, 405 | PLAYER_STRAIGHT: { x: 1085, y: 480, w: 80, h: 41 }, 406 | PLAYER_RIGHT: { x: 995, y: 531, w: 80, h: 41 } 407 | }; 408 | 409 | SPRITES.SCALE = 0.3 * (1/SPRITES.PLAYER_STRAIGHT.w) // the reference sprite width should be 1/3rd the (half-)roadWidth 410 | 411 | SPRITES.BILLBOARDS = [SPRITES.BILLBOARD01, SPRITES.BILLBOARD02, SPRITES.BILLBOARD03, SPRITES.BILLBOARD04, SPRITES.BILLBOARD05, SPRITES.BILLBOARD06, SPRITES.BILLBOARD07, SPRITES.BILLBOARD08, SPRITES.BILLBOARD09]; 412 | SPRITES.PLANTS = [SPRITES.TREE1, SPRITES.TREE2, SPRITES.DEAD_TREE1, SPRITES.DEAD_TREE2, SPRITES.PALM_TREE, SPRITES.BUSH1, SPRITES.BUSH2, SPRITES.CACTUS, SPRITES.STUMP, SPRITES.BOULDER1, SPRITES.BOULDER2, SPRITES.BOULDER3]; 413 | SPRITES.CARS = [SPRITES.CAR01, SPRITES.CAR02, SPRITES.CAR03, SPRITES.CAR04, SPRITES.SEMI, SPRITES.TRUCK]; 414 | 415 | -------------------------------------------------------------------------------- /images/background.js: -------------------------------------------------------------------------------- 1 | var BACKGROUND = { 2 | HILLS: { x: 5, y: 5, w: 1280, h: 480 }, 3 | SKY: { x: 5, y: 495, w: 1280, h: 480 }, 4 | TREES: { x: 5, y: 985, w: 1280, h: 480 } 5 | }; 6 | -------------------------------------------------------------------------------- /images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/background.png -------------------------------------------------------------------------------- /images/background/background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1922 | -------------------------------------------------------------------------------- /images/background/hills.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/background/hills.png -------------------------------------------------------------------------------- /images/background/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/background/sky.png -------------------------------------------------------------------------------- /images/background/trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/background/trees.png -------------------------------------------------------------------------------- /images/mute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/mute.png -------------------------------------------------------------------------------- /images/sprites.js: -------------------------------------------------------------------------------- 1 | var SPRITES = { 2 | PALM_TREE: { x: 5, y: 5, w: 215, h: 540 }, 3 | BILLBOARD08: { x: 230, y: 5, w: 385, h: 265 }, 4 | TREE1: { x: 625, y: 5, w: 360, h: 360 }, 5 | DEAD_TREE1: { x: 5, y: 555, w: 135, h: 332 }, 6 | BILLBOARD09: { x: 150, y: 555, w: 328, h: 282 }, 7 | BOULDER3: { x: 230, y: 280, w: 320, h: 220 }, 8 | COLUMN: { x: 995, y: 5, w: 200, h: 315 }, 9 | BILLBOARD01: { x: 625, y: 375, w: 300, h: 170 }, 10 | BILLBOARD06: { x: 488, y: 555, w: 298, h: 190 }, 11 | BILLBOARD05: { x: 5, y: 897, w: 298, h: 190 }, 12 | BILLBOARD07: { x: 313, y: 897, w: 298, h: 190 }, 13 | BOULDER2: { x: 621, y: 897, w: 298, h: 140 }, 14 | TREE2: { x: 1205, y: 5, w: 282, h: 295 }, 15 | BILLBOARD04: { x: 1205, y: 310, w: 268, h: 170 }, 16 | DEAD_TREE2: { x: 1205, y: 490, w: 150, h: 260 }, 17 | BOULDER1: { x: 1205, y: 760, w: 168, h: 248 }, 18 | BUSH1: { x: 5, y: 1097, w: 240, h: 155 }, 19 | CACTUS: { x: 929, y: 897, w: 235, h: 118 }, 20 | BUSH2: { x: 255, y: 1097, w: 232, h: 152 }, 21 | BILLBOARD03: { x: 5, y: 1262, w: 230, h: 220 }, 22 | BILLBOARD02: { x: 245, y: 1262, w: 215, h: 220 }, 23 | STUMP: { x: 995, y: 330, w: 195, h: 140 }, 24 | SEMI: { x: 1365, y: 490, w: 122, h: 144 }, 25 | TRUCK: { x: 1365, y: 644, w: 100, h: 78 }, 26 | CAR03: { x: 1383, y: 760, w: 88, h: 55 }, 27 | CAR02: { x: 1383, y: 825, w: 80, h: 59 }, 28 | CAR04: { x: 1383, y: 894, w: 80, h: 57 }, 29 | CAR01: { x: 1205, y: 1018, w: 80, h: 56 }, 30 | PLAYER_UPHILL_LEFT: { x: 1383, y: 961, w: 80, h: 45 }, 31 | PLAYER_UPHILL_STRAIGHT: { x: 1295, y: 1018, w: 80, h: 45 }, 32 | PLAYER_UPHILL_RIGHT: { x: 1385, y: 1018, w: 80, h: 45 }, 33 | PLAYER_LEFT: { x: 995, y: 480, w: 80, h: 41 }, 34 | PLAYER_STRAIGHT: { x: 1085, y: 480, w: 80, h: 41 }, 35 | PLAYER_RIGHT: { x: 995, y: 531, w: 80, h: 41 } 36 | }; 37 | -------------------------------------------------------------------------------- /images/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites.png -------------------------------------------------------------------------------- /images/sprites/billboard01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard01.png -------------------------------------------------------------------------------- /images/sprites/billboard02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard02.png -------------------------------------------------------------------------------- /images/sprites/billboard03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard03.png -------------------------------------------------------------------------------- /images/sprites/billboard04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard04.png -------------------------------------------------------------------------------- /images/sprites/billboard05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard05.png -------------------------------------------------------------------------------- /images/sprites/billboard06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard06.png -------------------------------------------------------------------------------- /images/sprites/billboard07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard07.png -------------------------------------------------------------------------------- /images/sprites/billboard08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard08.png -------------------------------------------------------------------------------- /images/sprites/billboard09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/billboard09.png -------------------------------------------------------------------------------- /images/sprites/boulder1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/boulder1.png -------------------------------------------------------------------------------- /images/sprites/boulder2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/boulder2.png -------------------------------------------------------------------------------- /images/sprites/boulder3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/boulder3.png -------------------------------------------------------------------------------- /images/sprites/bush1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/bush1.png -------------------------------------------------------------------------------- /images/sprites/bush2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/bush2.png -------------------------------------------------------------------------------- /images/sprites/cactus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/cactus.png -------------------------------------------------------------------------------- /images/sprites/car01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/car01.png -------------------------------------------------------------------------------- /images/sprites/car02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/car02.png -------------------------------------------------------------------------------- /images/sprites/car03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/car03.png -------------------------------------------------------------------------------- /images/sprites/car04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/car04.png -------------------------------------------------------------------------------- /images/sprites/column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/column.png -------------------------------------------------------------------------------- /images/sprites/dead_tree1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/dead_tree1.png -------------------------------------------------------------------------------- /images/sprites/dead_tree2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/dead_tree2.png -------------------------------------------------------------------------------- /images/sprites/palm_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/palm_tree.png -------------------------------------------------------------------------------- /images/sprites/player_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/player_left.png -------------------------------------------------------------------------------- /images/sprites/player_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/player_right.png -------------------------------------------------------------------------------- /images/sprites/player_straight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/player_straight.png -------------------------------------------------------------------------------- /images/sprites/player_uphill_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/player_uphill_left.png -------------------------------------------------------------------------------- /images/sprites/player_uphill_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/player_uphill_right.png -------------------------------------------------------------------------------- /images/sprites/player_uphill_straight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/player_uphill_straight.png -------------------------------------------------------------------------------- /images/sprites/semi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/semi.png -------------------------------------------------------------------------------- /images/sprites/stump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/stump.png -------------------------------------------------------------------------------- /images/sprites/tree1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/tree1.png -------------------------------------------------------------------------------- /images/sprites/tree2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/tree2.png -------------------------------------------------------------------------------- /images/sprites/truck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakesgordon/javascript-racer/3e8a060b5900755db27f899612a74a77427c853e/images/sprites/truck.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |15 | straight | 16 | curves | 17 | hills | 18 | final 19 | | 20 ||
24 | | 25 | 31 | | 32 |
---|---|
35 | | 36 | 42 | | 43 |
46 | | 47 | |
50 | | 51 | |
54 | | 55 | |
58 | | 59 | |
62 | | 63 | |
Use the arrow keys to drive the car.
68 |15 | straight | 16 | curves | 17 | hills | 18 | final 19 | | 20 ||
24 | | 25 | 31 | | 32 |
---|---|
35 | | 36 | 42 | | 43 |
46 | | 47 | |
50 | | 51 | |
54 | | 55 | |
58 | | 59 | |
62 | | 63 | |
Use the arrow keys to drive the car.
68 |15 | straight | 16 | curves | 17 | hills | 18 | final 19 | | 20 ||
24 | | 25 | 31 | | 32 |
---|---|
35 | | 36 | 42 | | 43 |
46 | | 47 | |
50 | | 51 | |
54 | | 55 | |
58 | | 59 | |
62 | | 63 | |
Use the arrow keys to drive the car.
68 |15 | straight | 16 | curves | 17 | hills | 18 | final 19 | | 20 ||
24 | | 25 | 31 | | 32 |
---|---|
35 | | 36 | 42 | | 43 |
46 | | 47 | |
50 | | 51 | |
54 | | 55 | |
58 | | 59 | |
62 | | 63 | |
Use the arrow keys to drive the car.
68 |