├── .gitattributes ├── .gitignore ├── README.md ├── assets ├── images │ ├── screenshot-01.jpg │ ├── screenshot-02.jpg │ └── screenshot-03.jpg └── sfx │ ├── alarm.mp3 │ ├── alien.mp3 │ ├── alien_drone.mp3 │ ├── explosion.mp3 │ ├── gun.mp3 │ ├── music.mp3 │ └── ship_drone.mp3 ├── gulpfile.js ├── index.html ├── index.src.html ├── package.json ├── scripts ├── alien.js ├── collisionDetector.js ├── controller.js ├── display.js ├── game.js ├── level.js ├── music.js ├── noise.js ├── sfx.js ├── ship.js ├── shot.js ├── track.js └── visualizer.js ├── styles ├── alien.less ├── app.css ├── app.less ├── background.less ├── game.less ├── ship.less ├── shot.less ├── track.less ├── vars.less └── visualizer.less └── vendor └── prefixfree.min.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | google-analytics.html 5 | soundcloud-id.js 6 | 7 | 8 | # Windows image file caches 9 | Thumbs.db 10 | ehthumbs.db 11 | 12 | # Folder config file 13 | Desktop.ini 14 | 15 | # Recycle Bin used on file shares 16 | $RECYCLE.BIN/ 17 | 18 | # Windows Installer files 19 | *.cab 20 | *.msi 21 | *.msm 22 | *.msp 23 | 24 | # ========================= 25 | # Operating System Files 26 | # ========================= 27 | 28 | # OSX 29 | # ========================= 30 | 31 | .DS_Store 32 | .AppleDouble 33 | .LSOverride 34 | 35 | # Icon must end with two \r 36 | Icon 37 | 38 | 39 | # Thumbnails 40 | ._* 41 | 42 | # Files that might appear on external disk 43 | .Spotlight-V100 44 | .Trashes 45 | 46 | # Directories potentially created on remote AFP share 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Space Shooter 2 | 3 | ![Screen shot](https://raw.githubusercontent.com/michaelbromley/css-space-shooter/master/assets/images/screenshot-02.jpg "Screen shot") 4 | 5 | ## [Play The Game](https://www.michaelbromley.co.uk/experiments/css-space-shooter/) 6 | 7 | This is an experiment I made to investigate the capabilities of CSS 3D transforms. 8 | Having played about with this technology a little (see [this](https://www.michaelbromley.co.uk/experiments/css-3d-butterfly/) or [this](http://www.michaelbromley.co.uk/horizonal/demo/), 9 | and having seen some very impressive demos ([CSS FPS](http://www.keithclark.co.uk/labs/css-fps/), [CSS X-Wing](http://codepen.io/juliangarnier/details/hzDAF), 10 | I wanted to explore the idea of making a simple 3D game with only DOM and CSS. 11 | 12 | ## Everything in CSS? Cool! 13 | 14 | [CSS transforms](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Using_CSS_transforms) allow us to position and rotate DOM elements in 3D space. The big advantage of this over, say, using canvas or webGL is that we do not need to 15 | worry about any of the complex maths involved in projecting a 3D object onto the screen. The browser's rendering engine (with the help of your GPU) will take care of all 16 | that. You just need to specify the x, y, z coordinates as well as the rotation along any axis. This makes it really simple to map your JavaScript objects onto the 17 | screen, by just keeping track of these simple coordinate and rotation values. 18 | 19 | Having [previously played with pseudo-3D in canvas](https://www.michaelbromley.co.uk/experiments/soundcloud-vis/#muse/undisclosed-desires), I have some idea 20 | of the massive amount of calculation involved in plotting all the lines and vertices of each 21 | object manually. In this regard, the simple, declarative nature of CSS allows some really powerful 3D effects with astonishingly little code. 22 | 23 | ## ...or not so cool. 24 | 25 | That convenience comes at a cost, however. For one, in CSS it is really really hard to create any shape other than a rectangle or an ellipse. Triangles, for example, are 26 | only possible through [dirty hacks with the border property](http://davidwalsh.name/css-triangles). 27 | 28 | Secondly, performance. Despite hardware acceleration for these 3D transforms, I quickly ran into performance issues when scaling up the number of objects 29 | interacting on screen simultaneously. Certain CSS operations are also *very* expensive, such as transitioning box-shadow values or gradient backgrounds. 30 | 31 | I'm sure my code can be optimized and this performance ceiling can be raised considerably. However, I wouldn't recommend using CSS and DOM for a serious 3D game. 32 | 33 | ## Browser Compatibility 34 | 35 | * Right now this works properly in the latest version of Chrome. 36 | * In my tests with Firefox it is very jerky and then usually grinds to a complete halt after a minute or so. 37 | * Internet Explorer has a couple of fatal issues - it does not yet support a key CSS property - [`transform-style: preserve3d`](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-style#Browser_compatibility) - 38 | which is essential to this method of building up 3D objects and 3D scenes which all share the same perspective. Additionally, IE does not currently support the 39 | Web Audio API, which I use for the sound effects and music. The game currently won't even load for this latter reason. 40 | * I've not tested in any other browsers, but feedback is welcome. 41 | 42 | ## Credits 43 | 44 | ### Inspiration and implementation details: 45 | 46 | * [Keith Clark](http://www.keithclark.co.uk/) - seriously, check out his stuff. It's amazing. Used his advice on positioning the DOM elements in the center of the viewport and moving them only 47 | with transforms, which works well. 48 | * html5Rocks - Some really helpful tutorials [here](http://www.html5rocks.com/en/tutorials/webaudio/games/) and [here](http://www.html5rocks.com/en/tutorials/webaudio/intro/) 49 | on how to use the Web Audio API. 50 | * Dive Into HTML5 [article on the localStorage API](http://diveintohtml5.info/storage.html), which I use to store high scores. 51 | 52 | ### Sound effects 53 | 54 | I got all my sounds effects from https://www.freesound.org. 55 | 56 | * gun: https://www.freesound.org/people/afirlam/sounds/236939/ 57 | * explosion: https://www.freesound.org/people/plamdi1/sounds/95058/ 58 | * alien noise: https://www.freesound.org/people/mensageirocs/sounds/234442/ 59 | * alien drone: https://www.freesound.org/people/klankbeeld/sounds/243702/ 60 | * 1-down: https://www.freesound.org/people/leviclaassen/sounds/107789/ 61 | 62 | ### Music 63 | 64 | Ludwig van Beethoven - Symphony No.7 in A major op.92 - II, Allegretto 65 | 66 | 67 | ## Developing 68 | 69 | ``` 70 | npm install 71 | npm run watch // dev mode 72 | npm run compile // production build 73 | ``` 74 | 75 | ## License 76 | 77 | MIT -------------------------------------------------------------------------------- /assets/images/screenshot-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/images/screenshot-01.jpg -------------------------------------------------------------------------------- /assets/images/screenshot-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/images/screenshot-02.jpg -------------------------------------------------------------------------------- /assets/images/screenshot-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/images/screenshot-03.jpg -------------------------------------------------------------------------------- /assets/sfx/alarm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/sfx/alarm.mp3 -------------------------------------------------------------------------------- /assets/sfx/alien.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/sfx/alien.mp3 -------------------------------------------------------------------------------- /assets/sfx/alien_drone.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/sfx/alien_drone.mp3 -------------------------------------------------------------------------------- /assets/sfx/explosion.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/sfx/explosion.mp3 -------------------------------------------------------------------------------- /assets/sfx/gun.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/sfx/gun.mp3 -------------------------------------------------------------------------------- /assets/sfx/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/sfx/music.mp3 -------------------------------------------------------------------------------- /assets/sfx/ship_drone.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/css-space-shooter/697751ffd7980226add0e94fe74c964683274f21/assets/sfx/ship_drone.mp3 -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 07/11/2014. 3 | */ 4 | 5 | var gulp = require('gulp'); 6 | var concat = require('gulp-concat'); 7 | var less = require('gulp-less'); 8 | var minifyCss = require('gulp-minify-css'); 9 | var uglify = require('gulp-uglify'); 10 | var inject = require('gulp-inject'); 11 | var rename = require('gulp-rename'); 12 | var merge = require('merge-stream'); 13 | 14 | 15 | gulp.task('scripts', function() { 16 | 17 | return gulp.src([ 18 | './scripts/!(game|controller|music)*.js', 19 | './scripts/music.js', 20 | './scripts/game.js', 21 | './scripts/controller.js' 22 | ]) 23 | .pipe(concat('script.js')) 24 | .pipe(uglify()) 25 | .pipe(gulp.dest('./dist/assets/')); 26 | }); 27 | 28 | gulp.task('less', function() { 29 | 30 | return gulp.src('./styles/app.less') 31 | .pipe(less()) 32 | .pipe(gulp.dest('./styles')); 33 | }); 34 | 35 | gulp.task('minify-css', ['less'], function() { 36 | 37 | return gulp.src('./styles/app.css') 38 | .pipe(minifyCss()) 39 | .pipe(gulp.dest('./dist/assets/')); 40 | }); 41 | 42 | gulp.task('static-assets', function() { 43 | return gulp.src([ 44 | './assets*/**/*', 45 | './vendor*/**/*' 46 | ]) 47 | .pipe(gulp.dest('./dist/')); 48 | }); 49 | 50 | gulp.task('build', ['less'], function() { 51 | 52 | var sourcesBuild = gulp.src([ 53 | './scripts/!(game|controller|music)*.js', 54 | './scripts/music.js', 55 | './scripts/game.js', 56 | './scripts/controller.js', 57 | './styles/app.css' 58 | ], {read: false}); 59 | 60 | 61 | return gulp.src('index.src.html') 62 | .pipe(inject(sourcesBuild, { addRootSlash: false })) 63 | .pipe(rename('index.html')) 64 | .pipe(gulp.dest('./')); 65 | 66 | }); 67 | 68 | gulp.task('inject-analytics', function() { 69 | 70 | return gulp.src('index.src.html') 71 | .pipe(inject(gulp.src(['./google-analytics.html']), { 72 | starttag: '', 73 | transform: function (filePath, file) { 74 | // return file contents as string 75 | return file.contents.toString('utf8') 76 | } 77 | })) 78 | .pipe(rename('index.html')) 79 | .pipe(gulp.dest('./dist')); 80 | }); 81 | 82 | gulp.task('compile', ['scripts', 'minify-css', 'static-assets', 'inject-analytics'], function() { 83 | 84 | var sourcesDist = gulp.src([ 85 | 'assets/*.js', 86 | 'assets/*.css' 87 | ], {read: false, cwd: 'dist'}); 88 | 89 | var index = gulp.src('index.html', {cwd: 'dist'}) 90 | .pipe(inject(sourcesDist, { addRootSlash: false })) 91 | .pipe(gulp.dest('dist')); 92 | 93 | }); 94 | 95 | gulp.task('default', ['static-assets', 'build'], function() { 96 | console.log('Watching JS files...'); 97 | console.log('Watching Less files...'); 98 | console.log('Watching index.src.html...'); 99 | gulp.watch('styles/*.less', ['less']); 100 | gulp.watch('index.src.html', ['build']); 101 | gulp.watch('scripts/*.js', ['scripts']); 102 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Space Shooter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 66 |
67 | 68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 |
81 | 82 | 83 |
84 | 87 |
88 |
89 |
90 |
91 | 94 | 99 | 100 |
101 | 102 |
103 | 105 |
106 |
107 |
CSS
108 |
Space
109 |
Shooter
110 |
111 | 112 |
113 |
Instructions
114 |
115 |

Pilot your ship with the 116 | , , & keys

117 |

Shoot with space

118 |

Pause / resume with p

119 |

You have 3 lives.

120 |

Don't let the aliens get past you!

121 |

OKAY

122 |
123 |
124 | 125 |
126 |
About
127 |
128 |

CSS Space Shooter is an experiment in 3D rendering using only the DOM and CSS transforms. 129 | No canvas, webGL or images of any kind are used to render the game.

130 |

Sound effects, music and audio visualization is handled by the Web Audio API.

131 |

All code is available on GitHub.

132 |

You can read more about it in this article.

133 |

© 2014 Michael Bromley.

134 |

OKAY

135 |
136 |
137 | 138 |

Press space to start!

139 |
140 | 141 | 168 |
169 | 170 |
171 |

Loading...

172 |
173 | 174 |
175 | This experiment might not work properly unless you run it in the latest versions of the Chrome browser. Sorry! 176 |
177 | 178 | 179 | 180 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /index.src.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Space Shooter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 |
66 | 67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | 79 |
80 | 81 | 82 |
83 | 86 |
87 |
88 |
89 |
90 | 93 | 98 | 99 |
100 | 101 |
102 | 104 |
105 |
106 |
CSS
107 |
Space
108 |
Shooter
109 |
110 | 111 |
112 |
Instructions
113 |
114 |

Pilot your ship with the 115 | , , & keys

116 |

Shoot with space

117 |

Pause / resume with p

118 |

You have 3 lives.

119 |

Don't let the aliens get past you!

120 |

OKAY

121 |
122 |
123 | 124 |
125 |
About
126 |
127 |

CSS Space Shooter is an experiment in 3D rendering using only the DOM and CSS transforms. 128 | No canvas, webGL or images of any kind are used to render the game.

129 |

Sound effects, music and audio visualization is handled by the Web Audio API.

130 |

All code is available on GitHub.

131 |

You can read more about it in this article.

132 |

© 2014 Michael Bromley.

133 |

OKAY

134 |
135 |
136 | 137 |

Press space to start!

138 |
139 | 140 | 167 |
168 | 169 |
170 |

Loading...

171 |
172 | 173 |
174 | This experiment might not work properly unless you run it in the latest versions of the Chrome browser. Sorry! 175 |
176 | 177 | 178 | 179 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-space-shooter", 3 | "version": "0.0.0", 4 | "description": "A 3D space shoot-em-up rendered with CSS", 5 | "main": "index.html", 6 | "scripts": { 7 | "watch": "gulp", 8 | "compile": "gulp compile" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/michaelbromley/css-space-shooter.git" 13 | }, 14 | "author": "Michael Bromley", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/michaelbromley/css-space-shooter/issues" 18 | }, 19 | "homepage": "https://github.com/michaelbromley/css-space-shooter", 20 | "devDependencies": { 21 | "gulp": "^3.8.10", 22 | "gulp-less": "^1.3.6", 23 | "gulp-uglify": "^1.0.1", 24 | "gulp-concat": "^2.4.1", 25 | "gulp-minify-css": "^0.3.11", 26 | "gulp-inject": "^1.0.2", 27 | "merge-stream": "^0.1.6", 28 | "gulp-rename": "^1.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/alien.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ALien class. 3 | * 4 | * @param el 5 | * @param x 6 | * @param y 7 | * @param config 8 | * @constructor 9 | */ 10 | function Alien(el, x, y, config) { 11 | var self = this; 12 | var zSpeed = 1000; // how fast it advances towards the player 13 | var range = -15000; 14 | self.el = el; 15 | self.el.classList.add(config.colorClass); 16 | self.x = x; 17 | self.y = y; 18 | self.z = range; 19 | self.actualX = x; // actual values include modifications made by the motion function, and should be 20 | self.actualY = y; // used by external methods to query the actual position of the alien. 21 | self.lastTimestamp = null; 22 | self.motionFunction = config.motionFunction; 23 | self.hit = false; // has the alien been hit by a shot? 24 | self.destroyed = false; // has it exploded from being hit? 25 | 26 | /** 27 | * The shipX and shipY is the position of the ship, which affects how the shots will be offset 28 | * @param shipX 29 | * @param shipY 30 | * @param timestamp 31 | * @returns {boolean} 32 | */ 33 | self.updatePosition = function(shipX, shipY, timestamp) { 34 | var actualPosition = applyMotionFunction(); 35 | var offsetX = self.x - shipX; 36 | var offsetY = self.y - shipY; 37 | var opacity = Math.min(1 - self.z / range / 2, 1); 38 | 39 | self.actualX = actualPosition.x; 40 | self.actualY = actualPosition.y; 41 | 42 | if (self.lastTimestamp === null || 43 | 100 < timestamp - self.lastTimestamp) { 44 | self.lastTimestamp = timestamp; 45 | } 46 | self.z += (timestamp - self.lastTimestamp) / 1000 * zSpeed; 47 | self.lastTimestamp = timestamp; 48 | 49 | self.el.style.transform = 50 | 'translateY(' + (actualPosition.y + offsetY) + 'px) ' + 51 | 'translateX(' + (actualPosition.x + offsetX) + 'px) ' + 52 | 'translateZ(' + self.z + 'px) '; 53 | self.el.style.opacity = opacity; 54 | self.el.style.display = 'block'; 55 | 56 | if (self.hit) { 57 | destroy(); 58 | } 59 | 60 | if (500 < self.z && self.hit === false) { 61 | emitMissEvent(); 62 | } 63 | 64 | return 500 < self.z || self.destroyed; 65 | }; 66 | 67 | function applyMotionFunction() { 68 | return self.motionFunction.call(self); 69 | } 70 | 71 | function destroy() { 72 | self.el.classList.add('hit'); 73 | setTimeout(function() { 74 | self.destroyed = true; 75 | }, 1200); 76 | } 77 | 78 | function emitMissEvent() { 79 | var event = new CustomEvent('miss', { 'detail': -500 }); 80 | document.dispatchEvent(event); 81 | } 82 | } 83 | 84 | 85 | var alienFactory = (function() { 86 | var alienElement; 87 | var aliens = []; 88 | var viewportWidth = document.documentElement.clientWidth; 89 | var viewportHeight = document.documentElement.clientHeight; 90 | 91 | return { 92 | 93 | setTemplate: function(el) { 94 | alienElement = el.cloneNode(true); 95 | }, 96 | 97 | spawn: function(event) { 98 | if (event.type && event.type === 'spawn') { 99 | event.data.forEach(function (alienDefinition) { 100 | 101 | var newElement = alienElement.cloneNode(true); 102 | var spawnX = viewportWidth * (Math.random() - 0.5) * 0.7; 103 | var spawnY = viewportHeight * (Math.random() - 0.5) * 0.5; 104 | var sceneDiv = document.querySelector('.scene'); 105 | var config = getAlienConfig(alienDefinition); 106 | 107 | sceneDiv.insertBefore(newElement, sceneDiv.children[0]); 108 | aliens.push(new Alien(newElement, spawnX, spawnY, config)); 109 | }); 110 | } 111 | }, 112 | 113 | updatePositions: function(ship, timestamp) { 114 | var el, remove, i, aliensToRemove = []; 115 | 116 | for(i = 0; i < aliens.length; i++) { 117 | remove = aliens[i].updatePosition(ship.x, ship.y, timestamp); 118 | if (remove) { 119 | aliensToRemove.push(i); 120 | } 121 | } 122 | 123 | // remove any aliens that have made it past the player 124 | for(i = aliensToRemove.length - 1; i >= 0; --i) { 125 | el = aliens[aliensToRemove[i]].el; 126 | var removedAliens = aliens.splice(aliensToRemove[i], 1); 127 | removedAliens[0].sound.stop(); 128 | document.querySelector('.scene').removeChild(el); 129 | } 130 | 131 | return aliensToRemove.length; 132 | }, 133 | 134 | aliens: function() { 135 | return aliens; 136 | } 137 | }; 138 | 139 | 140 | 141 | function getAlienConfig(alienDefinition) { 142 | var motionFunction, colorClass; 143 | 144 | /** 145 | * Alien motion functions. All take the z position of the alien as an argument, and return 146 | * an object with x and y properties. 147 | * The functions are called within the context of an alien object, so `this` will refer to 148 | * the alien itself. 149 | */ 150 | var noMotion = function() { 151 | return { 152 | x: this.x, 153 | y: this.y 154 | }; 155 | }; 156 | 157 | var verticalOscillation = function(speed) { 158 | return function() { 159 | var y = this.y + Math.sin(this.z / 1000 * speed) * viewportHeight / 4; 160 | var x = this.x; 161 | return { 162 | x: x, 163 | y: y 164 | }; 165 | } 166 | }; 167 | 168 | var horizontalOscillation = function(speed) { 169 | return function() { 170 | var y = this.y; 171 | var x = this.x + Math.sin(this.z / 1000 * speed) * viewportWidth / 4; 172 | return { 173 | x: x, 174 | y: y 175 | }; 176 | } 177 | }; 178 | 179 | var spiral = function(speed) { 180 | return function() { 181 | var y = this.y + Math.cos(this.z / 1000 * speed) * viewportWidth / 4; 182 | var x = this.x + Math.sin(this.z / 1000 * speed) * viewportWidth / 4; 183 | return { 184 | x: x, 185 | y: y 186 | }; 187 | } 188 | }; 189 | 190 | var random = function(speed) { 191 | var noiseX = new Simple1DNoise(); 192 | noiseX.setAmplitude(viewportWidth/2); 193 | var noiseY = new Simple1DNoise(); 194 | noiseY.setAmplitude(viewportHeight/2); 195 | 196 | return function() { 197 | var y = this.y + noiseY.getVal(this.z / 1000 * speed); 198 | var x = this.x + noiseX.getVal(this.z / 1000 * speed); 199 | return { 200 | x: x, 201 | y: y 202 | }; 203 | } 204 | }; 205 | 206 | if (alienDefinition.class === ALIEN_CLASS.stationary) { 207 | motionFunction = noMotion; 208 | colorClass = 'orange'; 209 | } else if (alienDefinition.class === ALIEN_CLASS.vertical) { 210 | motionFunction = verticalOscillation(alienDefinition.speed); 211 | colorClass = 'red'; 212 | } else if (alienDefinition.class === ALIEN_CLASS.horizontal) { 213 | motionFunction = horizontalOscillation(alienDefinition.speed); 214 | colorClass = 'blue'; 215 | } else if (alienDefinition.class === ALIEN_CLASS.spiral) { 216 | motionFunction = spiral(alienDefinition.speed); 217 | colorClass = 'green'; 218 | } else if (alienDefinition.class === ALIEN_CLASS.random) { 219 | motionFunction = random(alienDefinition.speed); 220 | colorClass = 'white'; 221 | } 222 | 223 | return { 224 | motionFunction: motionFunction, 225 | colorClass: colorClass 226 | }; 227 | } 228 | })(); -------------------------------------------------------------------------------- /scripts/collisionDetector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 04/10/2014. 3 | */ 4 | 5 | var collisionDetector = (function() { 6 | var module = {}; 7 | var screenWidth = document.documentElement.clientWidth; 8 | var screenHeight = document.documentElement.clientHeight; 9 | // dimensions of the alien's collision bounding box 10 | var alienBBZ = 100; 11 | var alienBBX = screenWidth * 0.01; 12 | var alienBBY = screenHeight * 0.01; 13 | 14 | module.check = function(shots, aliens) { 15 | aliens.forEach(function(alien) { 16 | 17 | shots.forEach(function(shot) { 18 | if (collision(alien, shot)) { 19 | if (!alien.hit) { 20 | alien.hit = true; 21 | emitHitEvent(alien); 22 | } 23 | shot.hit = true; 24 | } 25 | }); 26 | 27 | }); 28 | }; 29 | 30 | function collision(alien, shot) { 31 | var bbXScaled, bbYScaled; 32 | 33 | if (Math.abs(shot.z - alien.z) < alienBBZ) { 34 | bbXScaled = scaleBoundingBox(alienBBX, alien.z); 35 | bbYScaled = scaleBoundingBox(alienBBY, alien.z); 36 | if (Math.abs(shot.x - alien.actualX) < bbXScaled && Math.abs(shot.y - alien.actualY) < bbYScaled) { 37 | return true; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | function scaleBoundingBox(originalValue, zPosition) { 44 | var multiplier = (zPosition + 15000) / 1500; 45 | return originalValue * (1 + multiplier); 46 | } 47 | 48 | function emitHitEvent(alien) { 49 | var event = new CustomEvent('hit', { 'detail': { 50 | x: alien.x, 51 | y: alien.y, 52 | z: alien.z 53 | } }); 54 | document.dispatchEvent(event); 55 | } 56 | 57 | return module; 58 | })(); -------------------------------------------------------------------------------- /scripts/controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 15/10/2014. 3 | */ 4 | 5 | init(); 6 | 7 | function init() { 8 | 9 | 10 | game.init(function () { 11 | 12 | console.log('game loaded'); 13 | music.load('./assets/sfx/music.mp3', function () { 14 | document.querySelector('.loader').classList.add('hidden'); 15 | registerEventHandlers(); 16 | }); 17 | visualizer.setElement(document.querySelector('.visualizer')); 18 | }); 19 | 20 | game.onCompleted(function () { 21 | document.querySelector('.game-over-container').classList.remove('hidden'); 22 | document.querySelector('.game-won').classList.remove('hidden'); 23 | fillInScoreCard(); 24 | }); 25 | 26 | game.onDied(function () { 27 | document.querySelector('.game-over-container').classList.remove('hidden'); 28 | document.querySelector('.game-lost').classList.remove('hidden'); 29 | fillInScoreCard(); 30 | }); 31 | } 32 | 33 | function fillInScoreCard() { 34 | var data = game.getScoreCardInfo(); 35 | 36 | var bestScore = localStorage['bestScore']; 37 | var bestStage = localStorage['bestStage']; 38 | 39 | if (typeof bestScore === 'undefined' || parseInt(bestScore.replace(',',''), 10) < parseInt(data.score.replace(',',''), 10)) { 40 | document.querySelector('.new-record.score').style.display = 'inline'; 41 | localStorage['bestScore'] = bestScore = data.score; 42 | } 43 | if (typeof bestStage === 'undefined' || bestStage < data.stage) { 44 | document.querySelector('.new-record.stage').style.display = 'inline'; 45 | localStorage['bestStage'] = bestStage = data.stage; 46 | } 47 | 48 | document.querySelector('.stage-reached').innerText = data.stage; 49 | document.querySelector('.best-stage').innerText = bestStage || data.stage; 50 | document.querySelector('.score-achieved').innerText = data.score; 51 | document.querySelector('.best-score').innerText = bestScore || data.score; 52 | } 53 | 54 | 55 | function registerEventHandlers() { 56 | document.addEventListener('keydown', function (e) { 57 | var keyCode = e.which; 58 | if (keyCode === 32) { 59 | if (game.state() === 'initialized') { 60 | document.querySelector('.browser-warning').style.display = 'none'; 61 | game.start(); 62 | music.play(); 63 | visualizer.start(music); 64 | document.querySelector('.title-screen-container').classList.add('hidden'); 65 | } 66 | } 67 | if (keyCode === 80) { 68 | if (game.state() === 'paused') { 69 | game.resume(); 70 | music.resume(); 71 | } else { 72 | game.pause(); 73 | music.pause(); 74 | } 75 | } 76 | if (keyCode === 82) { 77 | if (game.state() === 'won' || game.state() === 'lost') { 78 | document.location.reload(); 79 | } 80 | } 81 | }); 82 | 83 | var instructionsDiv = document.querySelector('.instructions'); 84 | var aboutDiv = document.querySelector('.about'); 85 | 86 | instructionsDiv.querySelector('.open-instructions').addEventListener('click', function(e) { 87 | instructionsDiv.classList.add('display'); 88 | e.preventDefault(); 89 | }); 90 | instructionsDiv.querySelector('.close-instructions').addEventListener('click', function(e) { 91 | instructionsDiv.classList.remove('display'); 92 | e.preventDefault(); 93 | }); 94 | aboutDiv.querySelector('.open-about').addEventListener('click', function(e) { 95 | aboutDiv.classList.add('display'); 96 | e.preventDefault(); 97 | }); 98 | aboutDiv.querySelector('.close-about').addEventListener('click', function(e) { 99 | aboutDiv.classList.remove('display'); 100 | e.preventDefault(); 101 | }); 102 | } -------------------------------------------------------------------------------- /scripts/display.js: -------------------------------------------------------------------------------- 1 | function Announcer(el) { 2 | var self = this; 3 | self.container = el; 4 | self.showMessage = function(message, autoHide) { 5 | 6 | autoHide = autoHide || false; 7 | 8 | setTitle(message.title); 9 | setSubtitle(message.subtitle); 10 | self.container.classList.add('visible'); 11 | 12 | if (autoHide) { 13 | setTimeout(function () { 14 | self.hideMessage(); 15 | }, 2000); 16 | } 17 | }; 18 | 19 | self.hideMessage = function() { 20 | self.container.classList.remove('visible'); 21 | }; 22 | 23 | function setTitle(title) { 24 | self.container.querySelector('.title').innerHTML = (typeof title === 'undefined') ? '' : title; 25 | } 26 | 27 | function setSubtitle(subtitle) { 28 | self.container.querySelector('.subtitle').innerHTML = (typeof subtitle === 'undefined') ? '' : subtitle; 29 | } 30 | } 31 | 32 | 33 | var display = (function() { 34 | var module = {}; 35 | var announcer, firepowerContainer, score, livesContainer; 36 | 37 | module.setAnnouncerElement = function(el) { 38 | announcer = new Announcer(el); 39 | }; 40 | 41 | module.setFirepowerElement = function(el) { 42 | firepowerContainer = el; 43 | }; 44 | 45 | module.setScoreElement = function(el) { 46 | score = el; 47 | }; 48 | 49 | module.setLivesElement = function(el) { 50 | livesContainer = el; 51 | }; 52 | 53 | module.hideAll = function() { 54 | firepowerContainer.classList.add('hidden'); 55 | score.parentElement.classList.add('hidden'); 56 | livesContainer.classList.add('hidden'); 57 | }; 58 | 59 | module.showAll = function() { 60 | firepowerContainer.classList.remove('hidden'); 61 | score.parentElement.classList.remove('hidden'); 62 | livesContainer.classList.remove('hidden'); 63 | }; 64 | 65 | module.update = function(event, firepower, newScore) { 66 | if (event.type && event.type === 'announcement') { 67 | announcer.showMessage(event.data, true); 68 | } 69 | 70 | firepowerContainer.style.width = (firepower * 30) + 'px'; 71 | score.innerHTML = Math.round(newScore).toLocaleString(); 72 | }; 73 | 74 | module.showPausedMessage = function() { 75 | announcer.showMessage({ title: 'Paused', subtitle: 'Press "p" to resume'}); 76 | }; 77 | 78 | module.hidePausedMessage = function() { 79 | announcer.hideMessage(); 80 | }; 81 | 82 | module.updateLives = function(livesRemaining) { 83 | var i, totalLives = 3; 84 | 85 | for (i = totalLives; i > 0; i--) { 86 | if (i <= livesRemaining) { 87 | livesContainer.children[i-1].classList.remove('hidden'); 88 | } else { 89 | livesContainer.children[i - 1].classList.add('hidden'); 90 | } 91 | } 92 | }; 93 | 94 | return module; 95 | })(); -------------------------------------------------------------------------------- /scripts/game.js: -------------------------------------------------------------------------------- 1 | var game = (function() { 2 | 3 | var module = {}; 4 | 5 | /** 6 | * Globals 7 | */ 8 | var ship, 9 | track, 10 | hit, 11 | score = 0, 12 | lives = 3, 13 | keysDown = [], 14 | gameStarted = false, 15 | gamePaused = false, 16 | gameLost = false, 17 | gameWon = false, 18 | shipStartingX = 3000, 19 | shipStartingY = 6000, 20 | onCompleted, 21 | onDied; 22 | 23 | /** 24 | * Initialize 25 | */ 26 | module.init = function(callback) { 27 | 28 | ship = new Ship(document.querySelector('.ship-container'), 29 | document.documentElement.clientWidth, 30 | document.documentElement.clientHeight); 31 | ship.y = shipStartingY; 32 | ship.x = shipStartingX; 33 | track = new Track(document.querySelector('.midground')); 34 | 35 | display.setAnnouncerElement(document.querySelector('.announcement')); 36 | display.setFirepowerElement(document.querySelector('.firepower-meter-container')); 37 | display.setScoreElement(document.querySelector('.score')); 38 | display.setLivesElement(document.querySelector('.lives-container')); 39 | shotFactory.setTemplate(document.querySelector('.shot')); 40 | alienFactory.setTemplate(document.querySelector('.alien-container')); 41 | levelPlayer.setLevel(levelData); 42 | 43 | // set up the audio 44 | sfx.loadSounds(function() { 45 | callback(); 46 | }); 47 | 48 | window.requestAnimationFrame(tick); 49 | }; 50 | 51 | module.onCompleted = function(fn) { 52 | onCompleted = fn; 53 | }; 54 | 55 | module.onDied = function(fn) { 56 | onDied = fn; 57 | }; 58 | 59 | module.start = function() { 60 | gameStarted = true; 61 | 62 | display.showAll(); 63 | 64 | sfx.sounds.ship.play(ship.x, ship.y); 65 | 66 | setTimeout(track.start, 1000); 67 | 68 | registerEventHandlers(); 69 | hideCursor(); 70 | }; 71 | 72 | module.pause = function() { 73 | gamePaused = true; 74 | display.showPausedMessage(); 75 | track.stop(); 76 | sfx.setGain(0); 77 | showCursor(); 78 | }; 79 | 80 | module.resume = function() { 81 | gamePaused = false; 82 | display.hidePausedMessage(); 83 | track.start(); 84 | sfx.setGain(1); 85 | hideCursor(); 86 | requestAnimationFrame(tick); 87 | }; 88 | 89 | module.state = function() { 90 | var status; 91 | 92 | if (gameWon) { 93 | status = 'won'; 94 | } else if (gameLost) { 95 | status = 'lost'; 96 | } else if (!gameStarted) { 97 | status = 'initialized'; 98 | } else { 99 | if (gamePaused) { 100 | status = 'paused'; 101 | } else { 102 | status = 'running'; 103 | } 104 | } 105 | 106 | return status; 107 | }; 108 | 109 | module.getScoreCardInfo = function() { 110 | return { 111 | score: Math.round(score).toLocaleString(), 112 | stage: levelPlayer.getCurrentStage() 113 | }; 114 | }; 115 | 116 | /** 117 | * Game loop 118 | */ 119 | function tick(timestamp) { 120 | var event; 121 | 122 | if (!gameStarted) { 123 | ship.x = shipStartingX; 124 | ship.y = shipStartingY; 125 | } 126 | 127 | if (gameStarted && !gameLost) { 128 | if (0 < keysDown.length) { 129 | if (keysDown.indexOf(39) !== -1) { 130 | ship.moveLeft(); 131 | } 132 | if (keysDown.indexOf(37) !== -1) { 133 | ship.moveRight(); 134 | } 135 | if (keysDown.indexOf(38) !== -1) { 136 | ship.moveUp(); 137 | } 138 | if (keysDown.indexOf(40) !== -1) { 139 | ship.moveDown(); 140 | } 141 | if (keysDown.indexOf(32) !== -1) { 142 | shotFactory.create(ship); 143 | } 144 | } 145 | 146 | event = levelPlayer.getEvents(timestamp); 147 | 148 | alienFactory.spawn(event); 149 | display.update(event, shotFactory.firepower(), score); 150 | 151 | doSfx(); 152 | 153 | } 154 | 155 | 156 | ship.updatePosition(timestamp); 157 | track.update(ship); 158 | shotFactory.updatePositions(ship, timestamp); 159 | alienFactory.updatePositions(ship, timestamp); 160 | collisionDetector.check(shotFactory.shots(), alienFactory.aliens()); 161 | 162 | checkForGameOver(event, lives); 163 | 164 | if (!gamePaused) { 165 | window.requestAnimationFrame(tick); 166 | } 167 | } 168 | 169 | function doSfx() { 170 | sfx.sounds.ship.setParameters(ship.x, ship.y, ship.vx, ship.vy); 171 | 172 | // randomly make alien noises 173 | if (Math.random() < 0.001) { 174 | var aliens = alienFactory.aliens(); 175 | if (0 < aliens.length) { 176 | var alien = aliens[Math.floor(Math.random() * aliens.length)]; 177 | sfx.sounds.alien.play(alien.x, alien.y, alien.z); 178 | } 179 | } 180 | // update alien drone noises and add the sfx if not already there 181 | alienFactory.aliens().forEach(function(alien) { 182 | if (typeof alien.sound === 'undefined') { 183 | alien.sound = sfx.sounds.alienDrone.create(); 184 | } 185 | sfx.sounds.alienDrone.setParameters(alien.sound, alien, ship); 186 | }) 187 | } 188 | 189 | function checkForGameOver(event, lives) { 190 | if (event && event.type === 'completed') { 191 | if (!gameWon) { 192 | onCompleted(); 193 | gameWon = true; 194 | } 195 | } 196 | if (lives === 0) { 197 | if (!gameLost) { 198 | onDied(); 199 | gameLost = true; 200 | } 201 | } 202 | } 203 | 204 | function hideCursor() { 205 | document.body.style.cursor = 'none'; 206 | } 207 | function showCursor() { 208 | document.body.style.cursor = 'inherit'; 209 | } 210 | 211 | function registerEventHandlers() { 212 | /** 213 | * Event handlers 214 | */ 215 | document.addEventListener('keydown', function (e) { 216 | var keyCode = e.which; 217 | if (keysDown.indexOf(keyCode) === -1) { 218 | keysDown.push(keyCode); 219 | if (keyCode === 65) { 220 | alienFactory.spawn(); 221 | } 222 | } 223 | }); 224 | document.addEventListener('keyup', function (e) { 225 | var keyCode = e.which; 226 | keysDown.splice(keysDown.indexOf(keyCode), 1); 227 | }); 228 | 229 | document.addEventListener('hit', function (e) { 230 | var position = e.detail; 231 | score += 100 * shotFactory.firepower(); 232 | levelPlayer.alienRemoved(); 233 | sfx.sounds.explosion.play(position.x, position.y, position.z); 234 | }); 235 | 236 | document.addEventListener('shot', function (e) { 237 | sfx.sounds.gun.play(ship, e.detail.firepower); 238 | }); 239 | 240 | document.addEventListener('miss', function (e) { 241 | if (0 < lives) { 242 | lives--; 243 | display.updateLives(lives); 244 | sfx.sounds.alarm.play(); 245 | } 246 | levelPlayer.alienRemoved(); 247 | }); 248 | } 249 | 250 | return module; 251 | 252 | })(); -------------------------------------------------------------------------------- /scripts/level.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Michael on 08/10/2014. 3 | */ 4 | 5 | /** 6 | * Reads the levelData array and emits any events that occur at a given timestamp 7 | */ 8 | var levelPlayer = (function() { 9 | var module = {}; 10 | var lastTimeStamp, 11 | timeElapsed = 0, 12 | levelData, 13 | activeAliens = 0, 14 | currentStageIndex = 0, 15 | levelCompleted = false; 16 | 17 | module.setLevel = function(level) { 18 | levelData = JSON.parse(JSON.stringify(level)); 19 | }; 20 | 21 | module.getEvents = function(timestamp) { 22 | var currentStage; 23 | 24 | currentStage = getCurrentStage(); 25 | var secondsElapsed = getSecondsElapsed(timestamp); 26 | return getEventAtTime(secondsElapsed, currentStage); 27 | }; 28 | 29 | module.alienRemoved = function() { 30 | activeAliens = Math.max(activeAliens - 1, 0); 31 | }; 32 | 33 | module.getCurrentStage = function() { 34 | return Math.round(currentStageIndex + 1); 35 | }; 36 | 37 | function getSecondsElapsed(timestamp) { 38 | if (typeof lastTimeStamp === 'undefined' || 39 | 100 < timestamp - lastTimeStamp) { 40 | lastTimeStamp = timestamp; 41 | } 42 | timeElapsed += (timestamp - lastTimeStamp); 43 | lastTimeStamp = timestamp; 44 | return Math.floor(timeElapsed / 1000); 45 | } 46 | 47 | function getCurrentStage() { 48 | if (allStageEventsFired() && activeAliens === 0) { 49 | if (currentStageIndex < levelData.length - 1) { 50 | currentStageIndex++; 51 | activeAliens = 0; 52 | timeElapsed = 0; 53 | } else { 54 | levelCompleted = true; 55 | } 56 | } 57 | return levelData[currentStageIndex]; 58 | } 59 | 60 | function allStageEventsFired() { 61 | var stageEvents = levelData[currentStageIndex].events; 62 | return stageEvents[stageEvents.length - 1].fired === true; 63 | } 64 | 65 | function getEventAtTime(secondsElapsed, currentStage) { 66 | var e, event; 67 | 68 | if (!levelCompleted) { 69 | for (e = 0; e < currentStage.events.length; e++) { 70 | event = currentStage.events[e]; 71 | 72 | if (event.time === secondsElapsed) { 73 | if (!event.fired) { 74 | event.fired = true; 75 | setActiveAliens(event); 76 | return event; 77 | } 78 | } 79 | } 80 | return {}; 81 | } else { 82 | // return level complete event 83 | return { 84 | type: 'completed' 85 | }; 86 | } 87 | } 88 | 89 | function setActiveAliens(event) { 90 | if (event.type === 'spawn') { 91 | activeAliens += event.data.length; 92 | } 93 | } 94 | 95 | return module; 96 | })(); 97 | 98 | /** 99 | * An "enum" of the different types of alien 100 | * 101 | * @type {{stationary: number, vertical: number, horizontal: number, spiral: number, random: number}} 102 | */ 103 | var ALIEN_CLASS = { 104 | stationary: 1, 105 | vertical: 2, 106 | horizontal: 3, 107 | spiral: 4, 108 | random: 5 109 | }; 110 | 111 | /** 112 | * This array describes the game level structure. 113 | * 114 | * @type {{name: string, duration: number, sequence: {time: number, spawn: *[]}[]}[]} 115 | */ 116 | var levelData = [ 117 | { 118 | events: [ 119 | { time: 2, type: 'announcement', data: { title: 'Stage 1', subtitle: 'Flight School!'} }, 120 | { time: 11, type: 'spawn', data: [ 121 | { class: ALIEN_CLASS.stationary, speed: 1 } 122 | ] } 123 | ] 124 | }, 125 | { 126 | events: [ 127 | { time: 2, type: 'announcement', data: { title: 'Stage 2', subtitle: 'Warm-up!'} }, 128 | { time: 3, type: 'spawn', data: [ 129 | { class: ALIEN_CLASS.stationary, speed: 1 } 130 | ] }, 131 | { time: 7, type: 'spawn', data: [ 132 | { class: ALIEN_CLASS.stationary, speed: 1 } 133 | ] }, 134 | { time: 11, type: 'spawn', data: [ 135 | { class: ALIEN_CLASS.stationary, speed: 1 } 136 | ] }, 137 | { time: 16, type: 'spawn', data: [ 138 | { class: ALIEN_CLASS.stationary, speed: 1 }, 139 | { class: ALIEN_CLASS.stationary, speed: 1 } 140 | ] } 141 | ] 142 | }, 143 | { 144 | events: [ 145 | { time: 2, type: 'announcement', data: {title: 'Stage 3', subtitle: 'Ready?' } }, 146 | { time: 3, type: 'spawn', data: [ 147 | { class: ALIEN_CLASS.vertical, speed: 1 } 148 | ] }, 149 | { time: 6, type: 'spawn', data: [ 150 | { class: ALIEN_CLASS.horizontal, speed: 1 } 151 | ] }, 152 | { time: 9, type: 'spawn', data: [ 153 | { class: ALIEN_CLASS.vertical, speed: 1 }, 154 | { class: ALIEN_CLASS.stationary, speed: 1 } 155 | ] }, 156 | { time: 13, type: 'spawn', data: [ 157 | { class: ALIEN_CLASS.horizontal, speed: 1 }, 158 | { class: ALIEN_CLASS.stationary, speed: 1 } 159 | ] }, 160 | { time: 17, type: 'spawn', data: [ 161 | { class: ALIEN_CLASS.vertical, speed: 1.5 }, 162 | { class: ALIEN_CLASS.horizontal, speed: 1.5 } 163 | ] } 164 | ] 165 | }, 166 | { 167 | events: [ 168 | { time: 2, type: 'announcement', data: { title: 'Stage 4', subtitle: 'Spirals!'} }, 169 | { time: 3, type: 'spawn', data: [ 170 | { class: ALIEN_CLASS.spiral, speed: 1 } 171 | ] }, 172 | { time: 7, type: 'spawn', data: [ 173 | { class: ALIEN_CLASS.spiral, speed: 1 } 174 | ] }, 175 | { time: 11, type: 'spawn', data: [ 176 | { class: ALIEN_CLASS.spiral, speed: 1 } 177 | ] }, 178 | { time: 15, type: 'spawn', data: [ 179 | { class: ALIEN_CLASS.spiral, speed: 2 } 180 | ] }, 181 | { time: 16, type: 'spawn', data: [ 182 | { class: ALIEN_CLASS.spiral, speed: 2 } 183 | ] } 184 | ] 185 | }, 186 | { 187 | events: [ 188 | { time: 2, type: 'announcement', data: { title: 'Stage 5', subtitle: 'Turkey Shoot!'} }, 189 | { time: 3, type: 'spawn', data: [ 190 | { class: ALIEN_CLASS.stationary, speed: 1 } 191 | ] }, 192 | { time: 4, type: 'spawn', data: [ 193 | { class: ALIEN_CLASS.stationary, speed: 1 } 194 | ] }, 195 | { time: 5, type: 'spawn', data: [ 196 | { class: ALIEN_CLASS.stationary, speed: 1 } 197 | ] }, 198 | { time: 6, type: 'spawn', data: [ 199 | { class: ALIEN_CLASS.stationary, speed: 1 } 200 | ] }, 201 | { time: 7, type: 'spawn', data: [ 202 | { class: ALIEN_CLASS.stationary, speed: 1 } 203 | ] }, 204 | { time: 8, type: 'spawn', data: [ 205 | { class: ALIEN_CLASS.stationary, speed: 1 } 206 | ] }, 207 | { time: 8, type: 'spawn', data: [ 208 | { class: ALIEN_CLASS.stationary, speed: 1 } 209 | ] }, 210 | { time: 9, type: 'spawn', data: [ 211 | { class: ALIEN_CLASS.stationary, speed: 1 } 212 | ] }, 213 | { time: 10, type: 'spawn', data: [ 214 | { class: ALIEN_CLASS.stationary, speed: 1 } 215 | ] }, 216 | { time: 11, type: 'spawn', data: [ 217 | { class: ALIEN_CLASS.stationary, speed: 1 } 218 | ] }, 219 | { time: 12, type: 'spawn', data: [ 220 | { class: ALIEN_CLASS.stationary, speed: 1 } 221 | ] }, 222 | { time: 13, type: 'spawn', data: [ 223 | { class: ALIEN_CLASS.stationary, speed: 1 } 224 | ] }, 225 | { time: 14, type: 'spawn', data: [ 226 | { class: ALIEN_CLASS.stationary, speed: 1 }, 227 | { class: ALIEN_CLASS.stationary, speed: 1 } 228 | ] }, 229 | { time: 15, type: 'spawn', data: [ 230 | { class: ALIEN_CLASS.stationary, speed: 1 }, 231 | { class: ALIEN_CLASS.stationary, speed: 1 } 232 | ] }, 233 | { time: 16, type: 'spawn', data: [ 234 | { class: ALIEN_CLASS.stationary, speed: 1 }, 235 | { class: ALIEN_CLASS.stationary, speed: 1 } 236 | ] }, 237 | { time: 17, type: 'spawn', data: [ 238 | { class: ALIEN_CLASS.stationary, speed: 1 }, 239 | { class: ALIEN_CLASS.stationary, speed: 1 } 240 | ] }, 241 | { time: 18, type: 'spawn', data: [ 242 | { class: ALIEN_CLASS.stationary, speed: 1 }, 243 | { class: ALIEN_CLASS.stationary, speed: 1 } 244 | ] } 245 | ] 246 | }, 247 | { 248 | events: [ 249 | { time: 2, type: 'announcement', data: { title: 'Stage 6', subtitle: 'Catch Me If You Can!'} }, 250 | { time: 3, type: 'spawn', data: [ 251 | { class: ALIEN_CLASS.random, speed: 1 } 252 | ] }, 253 | { time: 8, type: 'spawn', data: [ 254 | { class: ALIEN_CLASS.random, speed: 3 } 255 | ] }, 256 | { time: 12, type: 'spawn', data: [ 257 | { class: ALIEN_CLASS.random, speed: 5 } 258 | ] } 259 | ] 260 | }, 261 | { 262 | events: [ 263 | { time: 2, type: 'announcement', data: { title: 'Stage 7', subtitle: 'A bit of everything!' } }, 264 | { time: 3, type: 'spawn', data: [{ class: ALIEN_CLASS.horizontal, speed: 2 }] }, 265 | { time: 5, type: 'spawn', data: [{ class: ALIEN_CLASS.vertical, speed: 2 }] }, 266 | { time: 7, type: 'spawn', data: [{ class: ALIEN_CLASS.spiral, speed: 2 }] }, 267 | { time: 9, type: 'spawn', data: [{ class: ALIEN_CLASS.stationary, speed: 1 }] }, 268 | { time: 11, type: 'spawn', data: [{ class: ALIEN_CLASS.vertical, speed: 3 }] }, 269 | { time: 14, type: 'spawn', data: [{ class: ALIEN_CLASS.horizontal, speed: 3 }] }, 270 | { time: 17, type: 'spawn', data: [{ class: ALIEN_CLASS.stationary, speed: 1 }] }, 271 | { time: 20, type: 'spawn', data: [ 272 | { class: ALIEN_CLASS.spiral, speed: 2 }, 273 | { class: ALIEN_CLASS.spiral, speed: 3 } 274 | ] } 275 | ] 276 | }, 277 | { 278 | events: [ 279 | { time: 2, type: 'announcement', data: { title: 'Stage 8', subtitle: 'Don\'t Panic!'} }, 280 | { time: 3, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 3 }] }, 281 | { time: 6, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 3 }] }, 282 | { time: 7, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 4 }] }, 283 | { time: 9, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 4 }] }, 284 | { time: 15, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 5 }] }, 285 | { time: 17, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 5 }] }, 286 | { time: 19, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 5 }] }, 287 | { time: 20, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 5 }] }, 288 | { time: 21, type: 'spawn', data: [ { class: ALIEN_CLASS.random, speed: 5 }] } 289 | ] 290 | }, 291 | { 292 | events: [ 293 | { time: 2, type: 'announcement', data: { title: 'Stage 9', subtitle: 'Hang In There!'} }, 294 | { time: 3, type: 'spawn', data: [ 295 | { class: ALIEN_CLASS.horizontal, speed: 3 }, 296 | { class: ALIEN_CLASS.vertical, speed: 3 } 297 | ] }, 298 | { time: 6, type: 'spawn', data: [ 299 | { class: ALIEN_CLASS.stationary, speed: 3 }, 300 | { class: ALIEN_CLASS.spiral, speed: 3 } 301 | ] }, 302 | { time: 9, type: 'spawn', data: [ 303 | { class: ALIEN_CLASS.random, speed: 2 }, 304 | { class: ALIEN_CLASS.random, speed: 4 } 305 | ] }, 306 | { time: 11, type: 'spawn', data: [ 307 | { class: ALIEN_CLASS.horizontal, speed: 4 }, 308 | { class: ALIEN_CLASS.random, speed: 3 } 309 | ] }, 310 | { time: 13, type: 'spawn', data: [ 311 | { class: ALIEN_CLASS.stationary, speed: 3 }, 312 | { class: ALIEN_CLASS.stationary, speed: 3 } 313 | ] }, 314 | { time: 14, type: 'spawn', data: [ 315 | { class: ALIEN_CLASS.spiral, speed: 3 }, 316 | { class: ALIEN_CLASS.stationary, speed: 3 }, 317 | { class: ALIEN_CLASS.spiral, speed: 5 } 318 | ] }, 319 | { time: 15, type: 'spawn', data: [ 320 | { class: ALIEN_CLASS.horizontal, speed: 4 }, 321 | { class: ALIEN_CLASS.stationary, speed: 4 }, 322 | { class: ALIEN_CLASS.vertical, speed: 3 } 323 | ] }, 324 | { time: 16, type: 'spawn', data: [ 325 | { class: ALIEN_CLASS.vertical, speed: 3 }, 326 | { class: ALIEN_CLASS.random, speed: 3 }, 327 | { class: ALIEN_CLASS.stationary, speed: 3 } 328 | ] }, 329 | { time: 21, type: 'spawn', data: [ 330 | { class: ALIEN_CLASS.random, speed: 3 }, 331 | { class: ALIEN_CLASS.stationary, speed: 3 }, 332 | { class: ALIEN_CLASS.spiral, speed: 3 }, 333 | { class: ALIEN_CLASS.stationary, speed: 3 } 334 | ] }, 335 | { time: 22, type: 'spawn', data: [ 336 | { class: ALIEN_CLASS.random, speed: 4 }, 337 | { class: ALIEN_CLASS.random, speed: 4 }, 338 | { class: ALIEN_CLASS.stationary, speed: 4 }, 339 | { class: ALIEN_CLASS.stationary, speed: 4 }, 340 | { class: ALIEN_CLASS.spiral, speed: 4 } 341 | ] }, 342 | { time: 23, type: 'spawn', data: [ 343 | { class: ALIEN_CLASS.random, speed: 4 }, 344 | { class: ALIEN_CLASS.random, speed: 4 }, 345 | { class: ALIEN_CLASS.spiral, speed: 4 } 346 | ] }, 347 | { time: 26, type: 'spawn', data: [ 348 | { class: ALIEN_CLASS.random, speed: 4 }, 349 | { class: ALIEN_CLASS.random, speed: 4 }, 350 | { class: ALIEN_CLASS.stationary, speed: 4 }, 351 | { class: ALIEN_CLASS.stationary, speed: 4 }, 352 | { class: ALIEN_CLASS.spiral, speed: 4 } 353 | ] } 354 | ] 355 | }, 356 | { 357 | events: [ 358 | { time: 2, type: 'announcement', data: { title: 'Stage 10', subtitle: 'Final Stage!'} }, 359 | { time: 3, type: 'spawn', data: [ { class: ALIEN_CLASS.stationary, speed: 3 }] }, 360 | { time: 8, type: 'spawn', data: [ { class: ALIEN_CLASS.stationary, speed: 3 }] }, 361 | { time: 12, type: 'spawn', data: [ { class: ALIEN_CLASS.stationary, speed: 4 }] }, 362 | { time: 15, type: 'spawn', data: [ 363 | { class: ALIEN_CLASS.stationary, speed: 4 }, 364 | { class: ALIEN_CLASS.stationary, speed: 4 }, 365 | { class: ALIEN_CLASS.stationary, speed: 4 } 366 | ] }, 367 | { time: 18, type: 'spawn', data: [ 368 | { class: ALIEN_CLASS.stationary, speed: 4 }, 369 | { class: ALIEN_CLASS.stationary, speed: 4 }, 370 | { class: ALIEN_CLASS.stationary, speed: 4 } 371 | ] }, 372 | { time: 22, type: 'spawn', data: [ 373 | { class: ALIEN_CLASS.stationary, speed: 4 }, 374 | { class: ALIEN_CLASS.stationary, speed: 4 }, 375 | { class: ALIEN_CLASS.stationary, speed: 4 } 376 | ] }, 377 | { time: 25, type: 'spawn', data: [ 378 | { class: ALIEN_CLASS.stationary, speed: 4 }, 379 | { class: ALIEN_CLASS.stationary, speed: 4 }, 380 | { class: ALIEN_CLASS.stationary, speed: 4 } 381 | ] }, 382 | { time: 30, type: 'spawn', data: [ 383 | { class: ALIEN_CLASS.vertical, speed: 1 }, 384 | { class: ALIEN_CLASS.horizontal, speed: 1 }, 385 | { class: ALIEN_CLASS.vertical, speed: 5 }, 386 | { class: ALIEN_CLASS.horizontal, speed: 5 } 387 | ] }, 388 | { time: 35, type: 'spawn', data: [ 389 | { class: ALIEN_CLASS.vertical, speed: 1 }, 390 | { class: ALIEN_CLASS.horizontal, speed: 1 }, 391 | { class: ALIEN_CLASS.vertical, speed: 2 }, 392 | { class: ALIEN_CLASS.spiral, speed: 2 } 393 | ] }, 394 | { time: 40, type: 'spawn', data: [ 395 | { class: ALIEN_CLASS.spiral, speed: 1 }, 396 | { class: ALIEN_CLASS.vertical, speed: 1 }, 397 | { class: ALIEN_CLASS.vertical, speed: 3 }, 398 | { class: ALIEN_CLASS.spiral, speed: 5 } 399 | ] }, 400 | { time: 45, type: 'spawn', data: [{ class: ALIEN_CLASS.random, speed: 1 } ]}, 401 | { time: 46, type: 'spawn', data: [{ class: ALIEN_CLASS.random, speed: 1 } ]}, 402 | { time: 47, type: 'spawn', data: [{ class: ALIEN_CLASS.random, speed: 2 } ]}, 403 | { time: 48, type: 'spawn', data: [{ class: ALIEN_CLASS.random, speed: 2 } ]}, 404 | { time: 49, type: 'spawn', data: [{ class: ALIEN_CLASS.random, speed: 3 } ]}, 405 | { time: 50, type: 'spawn', data: [{ class: ALIEN_CLASS.random, speed: 3 } ]}, 406 | { time: 51, type: 'spawn', data: [{ class: ALIEN_CLASS.random, speed: 4 } ]}, 407 | { time: 52, type: 'spawn', data: [{ class: ALIEN_CLASS.random, speed: 5 } ]} 408 | ] 409 | } 410 | ]; 411 | -------------------------------------------------------------------------------- /scripts/music.js: -------------------------------------------------------------------------------- 1 | var music = (function() { 2 | var module = {}; 3 | 4 | var player = document.getElementById('player'); 5 | var loader = new MP3Loader(player); 6 | var audioSource = new MP3AudioSource(player); 7 | 8 | module.load = function(trackUrl, callback) { 9 | callback = callback || function() {}; 10 | loader.loadStream(trackUrl, 11 | callback, 12 | function() { 13 | console.log("Error: ", loader.errorMessage); 14 | } 15 | ); 16 | }; 17 | 18 | module.play = function() { 19 | if (loader.successfullyLoaded) { 20 | audioSource.playStream(loader.streamUrl()); 21 | } 22 | }; 23 | 24 | module.pause = function() { 25 | player.pause(); 26 | }; 27 | 28 | module.resume = function() { 29 | player.play(); 30 | }; 31 | 32 | module.getAudioData = function() { 33 | return { 34 | volume: audioSource.volume, 35 | frequencyData: audioSource.streamData 36 | }; 37 | }; 38 | 39 | return module; 40 | })(); 41 | 42 | 43 | 44 | function SoundCloudAudioSource(player) { 45 | var self = this; 46 | var analyser; 47 | var audioCtx = new (window.AudioContext || window.webkitAudioContext); 48 | analyser = audioCtx.createAnalyser(); 49 | analyser.fftSize = 256; 50 | var source = audioCtx.createMediaElementSource(player); 51 | source.connect(analyser); 52 | analyser.connect(audioCtx.destination); 53 | var sampleAudioStream = function () { 54 | analyser.getByteFrequencyData(self.streamData); 55 | // calculate an overall volume value 56 | var total = 0; 57 | for (var i = 0; i < 80; i++) { // get the volume from the first 80 bins, else it gets too loud with treble 58 | total += self.streamData[i]; 59 | } 60 | self.volume = total; 61 | }; 62 | setInterval(sampleAudioStream, 20); 63 | // public properties and methods 64 | this.volume = 0; 65 | this.streamData = new Uint8Array(128); 66 | this.playStream = function (streamUrl) { 67 | // get the input stream from the audio element 68 | player.addEventListener('ended', function () { 69 | self.directStream('coasting'); 70 | }); 71 | player.setAttribute('src', streamUrl); 72 | player.play(); 73 | } 74 | } 75 | 76 | /** 77 | * Makes a request to the Soundcloud API and returns the JSON data. 78 | */ 79 | function SoundcloudLoader(player) { 80 | var self = this; 81 | var client_id = SOUNDCLOUD_ID; // to get an ID go to http://developers.soundcloud.com/ 82 | this.sound = {}; 83 | this.streamUrl = ""; 84 | this.errorMessage = ""; 85 | this.player = player; 86 | this.successfullyLoaded = false; 87 | 88 | /** 89 | * Loads the JSON stream data object from the URL of the track (as given in the location bar of the browser when browsing Soundcloud), 90 | * and on success it calls the callback passed to it (for example, used to then send the stream_url to the audiosource object). 91 | * @param track_url 92 | * @param callback 93 | */ 94 | this.loadStream = function(track_url, successCallback, errorCallback) { 95 | if (typeof SC !== 'undefined') { 96 | SC.initialize({ 97 | client_id: client_id 98 | }); 99 | SC.get('/resolve', {url: track_url}, function (sound) { 100 | if (sound) { 101 | if (sound.errors) { 102 | self.errorMessage = ""; 103 | for (var i = 0; i < sound.errors.length; i++) { 104 | self.errorMessage += sound.errors[i].error_message + '
'; 105 | } 106 | self.errorMessage += 'Make sure the URL has the correct format: https://soundcloud.com/user/title-of-the-track'; 107 | errorCallback(); 108 | } else { 109 | 110 | self.successfullyLoaded = true; 111 | console.log('music loaded'); 112 | 113 | if (sound.kind == "playlist") { 114 | self.sound = sound; 115 | self.streamPlaylistIndex = 0; 116 | self.streamUrl = function () { 117 | return sound.tracks[self.streamPlaylistIndex].stream_url + '?client_id=' + client_id; 118 | }; 119 | successCallback(); 120 | } else { 121 | self.sound = sound; 122 | self.streamUrl = function () { 123 | return sound.stream_url + '?client_id=' + client_id; 124 | }; 125 | successCallback(); 126 | } 127 | } 128 | } else { 129 | console.log('An unspecified error occurred. No music could be loaded'); 130 | successCallback(); // call success just so the game will still run 131 | } 132 | }); 133 | } else { 134 | console.log('SoundCloud library not found. No music could be loaded'); 135 | successCallback(); // call success just so the game will still run 136 | } 137 | }; 138 | 139 | 140 | this.directStream = function(direction){ 141 | if(direction=='toggle'){ 142 | if (this.player.paused) { 143 | this.player.play(); 144 | } else { 145 | this.player.pause(); 146 | } 147 | } 148 | else if(this.sound.kind=="playlist"){ 149 | if(direction=='coasting') { 150 | this.streamPlaylistIndex++; 151 | }else if(direction=='forward') { 152 | if(this.streamPlaylistIndex>=this.sound.track_count-1) this.streamPlaylistIndex = 0; 153 | else this.streamPlaylistIndex++; 154 | }else{ 155 | if(this.streamPlaylistIndex<=0) this.streamPlaylistIndex = this.sound.track_count-1; 156 | else this.streamPlaylistIndex--; 157 | } 158 | if(this.streamPlaylistIndex>=0 && this.streamPlaylistIndex<=this.sound.track_count-1) { 159 | this.player.setAttribute('src',this.streamUrl()); 160 | this.player.play(); 161 | } 162 | } 163 | } 164 | } 165 | 166 | function MP3AudioSource(player) { 167 | var self = this; 168 | var analyser; 169 | var audioCtx = new (window.AudioContext || window.webkitAudioContext); 170 | analyser = audioCtx.createAnalyser(); 171 | analyser.fftSize = 256; 172 | var source = audioCtx.createMediaElementSource(player); 173 | source.connect(analyser); 174 | analyser.connect(audioCtx.destination); 175 | var sampleAudioStream = function () { 176 | analyser.getByteFrequencyData(self.streamData); 177 | // calculate an overall volume value 178 | var total = 0; 179 | for (var i = 0; i < 80; i++) { // get the volume from the first 80 bins, else it gets too loud with treble 180 | total += self.streamData[i]; 181 | } 182 | self.volume = total; 183 | }; 184 | setInterval(sampleAudioStream, 20); 185 | // public properties and methods 186 | this.volume = 0; 187 | this.streamData = new Uint8Array(128); 188 | this.playStream = function (streamUrl) { 189 | // get the input stream from the audio element 190 | player.addEventListener('ended', function () { 191 | player.play(); 192 | }); 193 | player.setAttribute('src', streamUrl); 194 | player.play(); 195 | } 196 | } 197 | 198 | /** 199 | * Loads an mp3 file 200 | */ 201 | function MP3Loader(player) { 202 | var self = this; 203 | this.sound = {}; 204 | this.streamUrl = function () { return ''; }; 205 | this.errorMessage = ""; 206 | this.player = player; 207 | this.successfullyLoaded = false; 208 | 209 | this.loadStream = function(track_url, successCallback, errorCallback) { 210 | self.successfullyLoaded = true; 211 | this.streamUrl = function () { return track_url; }; 212 | successCallback(); 213 | }; 214 | } 215 | 216 | -------------------------------------------------------------------------------- /scripts/noise.js: -------------------------------------------------------------------------------- 1 | function Simple1DNoise() { 2 | var MAX_VERTICES = 256; 3 | var MAX_VERTICES_MASK = MAX_VERTICES -1; 4 | var amplitude = 1; 5 | var scale = 1; 6 | 7 | var r = []; 8 | 9 | for ( var i = 0; i < MAX_VERTICES; ++i ) { 10 | r.push(Math.random()); 11 | } 12 | 13 | var getVal = function( x ){ 14 | var scaledX = x * scale; 15 | var xFloor = Math.floor(scaledX); 16 | var t = scaledX - xFloor; 17 | var tRemapSmoothstep = t * t * ( 3 - 2 * t ); 18 | 19 | /// Modulo using & 20 | var xMin = xFloor & MAX_VERTICES_MASK; 21 | var xMax = ( xMin + 1 ) & MAX_VERTICES_MASK; 22 | 23 | var y = lerp( r[ xMin ], r[ xMax ], tRemapSmoothstep ); 24 | 25 | return y * amplitude; 26 | }; 27 | 28 | /** 29 | * Linear interpolation function. 30 | * @param a The lower integer value 31 | * @param b The upper integer value 32 | * @param t The value between the two 33 | * @returns {number} 34 | */ 35 | var lerp = function(a, b, t ) { 36 | return a * ( 1 - t ) + b * t; 37 | }; 38 | 39 | // return the API 40 | return { 41 | getVal: getVal, 42 | setAmplitude: function(newAmplitude) { 43 | amplitude = newAmplitude; 44 | }, 45 | setScale: function(newScale) { 46 | scale = newScale; 47 | } 48 | }; 49 | } -------------------------------------------------------------------------------- /scripts/sfx.js: -------------------------------------------------------------------------------- 1 | var sfx = (function() { 2 | var module = {}; 3 | 4 | // Fix up prefixing 5 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 6 | var context = new AudioContext(); 7 | var masterGain = context.createGain(); 8 | masterGain.connect(context.destination); 9 | 10 | module.sounds = {}; 11 | 12 | module.loadSounds = function(callback) { 13 | var bufferLoader = new BufferLoader( 14 | context, 15 | [ 16 | 'assets/sfx/gun.mp3', 17 | 'assets/sfx/ship_drone.mp3', 18 | 'assets/sfx/explosion.mp3', 19 | 'assets/sfx/alien.mp3', 20 | 'assets/sfx/alien_drone.mp3', 21 | 'assets/sfx/alarm.mp3' 22 | ], 23 | finishedLoading 24 | ); 25 | 26 | bufferLoader.load(); 27 | 28 | function finishedLoading(bufferList) { 29 | 30 | var sfxGun = new Sound(bufferList[0], context); 31 | var sfxShip = new Sound(bufferList[1], context); 32 | var sfxExplosion = new Sound(bufferList[2], context); 33 | var sfxAlien = new Sound(bufferList[3], context); 34 | var sfxAlarm = new Sound(bufferList[5], context); 35 | 36 | // set some initial values 37 | sfxExplosion.setGain(2); 38 | sfxAlien.setGain(2); 39 | sfxGun.setPannerParameters({ 40 | coneOuterGain: 0.9, 41 | coneOuterAngle: 40, 42 | coneInnerAngle: 0, 43 | rolloffFactor: 0.1 44 | }); 45 | sfxGun.setGain(0.1); 46 | sfxShip.setPannerParameters({ 47 | coneOuterGain: 1, 48 | coneOuterAngle: 360, 49 | coneInnerAngle: 0, 50 | rolloffFactor: 0.3 51 | }); 52 | sfxShip.setGain(2.5); 53 | 54 | module.sounds = { 55 | gun: { 56 | play: function(ship, firepower) { 57 | x = ship.x / 100; 58 | y = ship.y / 100; 59 | vx = ship.vx / 5; 60 | vy = ship.vy / 5; 61 | sfxGun.setPosition(x, y, -3); 62 | // sfxGun.setVelocity(vx, vy, 0); 63 | var playbackRate = 0.5 + firepower / 20; 64 | sfxGun.setPlaybackRate(playbackRate); 65 | sfxGun.play(masterGain); 66 | } 67 | }, 68 | ship: { 69 | play: function(x, y) { 70 | x /= 100; 71 | y /= 100; 72 | sfxShip.setPosition(x, y, -3); 73 | sfxShip.play(masterGain, true); 74 | }, 75 | setParameters: function(x, y, vx, vy) { 76 | x /= 50; 77 | y /= 50; 78 | vx /= 10; 79 | vy /= 10; 80 | sfxShip.setPosition(x, y, -3); 81 | // sfxShip.setVelocity(vx, vy, 0); 82 | } 83 | }, 84 | explosion: { 85 | play: function(x, y, z) { 86 | x /= 100; 87 | y /= 100; 88 | z /= 1000; 89 | sfxExplosion.setPosition(x, y, z); 90 | sfxExplosion.play(masterGain); 91 | } 92 | }, 93 | alien: { 94 | play: function(x, y, z) { 95 | x /= 100; 96 | y /= 100; 97 | z /= 1000; 98 | sfxAlien.setPosition(x, y, z); 99 | sfxAlien.play(masterGain); 100 | } 101 | }, 102 | alienDrone: { 103 | create: function() { 104 | var sfxAlienDrone = new Sound(bufferList[4], context); 105 | sfxAlienDrone.setPannerParameters({ 106 | coneOuterGain: 0.1, 107 | coneOuterAngle: 90, 108 | coneInnerAngle: 0, 109 | rolloffFactor: 2 110 | }); 111 | sfxAlienDrone.setGain(1.5); 112 | sfxAlienDrone.play(masterGain, true); 113 | return sfxAlienDrone; 114 | }, 115 | /** 116 | * We take the alien and the ship as parameters so we can calculate the distance between the two, 117 | * which determines the panning. 118 | * @param sound 119 | * @param alien 120 | * @param ship 121 | */ 122 | setParameters: function(sound, alien, ship) { 123 | x = (alien.x - ship.x) / 100; 124 | y = (alien.y - ship.y) / 100; 125 | z = alien.z / 1000; 126 | sound.setPosition(x, y, z); 127 | } 128 | }, 129 | alarm: { 130 | play: function() { 131 | sfxAlarm.play(masterGain); 132 | } 133 | } 134 | }; 135 | callback(); 136 | } 137 | }; 138 | 139 | module.setGain = function(value) { 140 | masterGain.gain.value = value; 141 | }; 142 | 143 | return module; 144 | })(); 145 | 146 | 147 | 148 | /** 149 | * Object representing a sound sample, containing all audio nodes and a play method. 150 | * 151 | * @param buffer 152 | * @param context 153 | * @constructor 154 | */ 155 | function Sound(buffer, context) { 156 | this.context = context; 157 | this.buffer = buffer; 158 | this.panner = context.createPanner(); 159 | this.gain = context.createGain(); 160 | this.playbackRate = 1; 161 | 162 | this.setPannerParameters = function(options) { 163 | for(var option in options) { 164 | if (options.hasOwnProperty(option)) { 165 | this.panner[option] = options[option]; 166 | } 167 | } 168 | }; 169 | 170 | this.setPlaybackRate = function(value) { 171 | this.playbackRate = value; 172 | }; 173 | 174 | this.setGain = function(value) { 175 | this.gain.gain.value = value; 176 | }; 177 | 178 | this.setPosition = function(x, y, z) { 179 | this.panner.setPosition(x, y, z); 180 | }; 181 | 182 | this.setVelocity = function(vx, vy, vz) { 183 | // this.panner.setVelocity(vx, vy, vz); 184 | }; 185 | 186 | this.play = function(outputNode, loop) { 187 | loop = loop || false; 188 | this.source = this.context.createBufferSource(); 189 | this.source.buffer = this.buffer; 190 | this.source.playbackRate.value = this.playbackRate; 191 | if (loop) { 192 | this.source.loop = true; 193 | } 194 | this.source.connect(this.gain); 195 | this.gain.connect(this.panner); 196 | this.panner.connect(outputNode); 197 | this.source.start(); 198 | }; 199 | 200 | this.stop = function() { 201 | this.source.stop(); 202 | }; 203 | } 204 | 205 | 206 | 207 | 208 | /** 209 | * Taken from http://www.html5rocks.com/en/tutorials/webaudio/intro/ 210 | * @param context 211 | * @param urlList 212 | * @param callback 213 | * @constructor 214 | */ 215 | function BufferLoader(context, urlList, callback) { 216 | this.context = context; 217 | this.urlList = urlList; 218 | this.onload = callback; 219 | this.bufferList = []; 220 | this.loadCount = 0; 221 | } 222 | 223 | BufferLoader.prototype.loadBuffer = function(url, index) { 224 | // Load buffer asynchronously 225 | var request = new XMLHttpRequest(); 226 | request.open("GET", url, true); 227 | request.responseType = "arraybuffer"; 228 | 229 | var loader = this; 230 | 231 | request.onload = function() { 232 | // Asynchronously decode the audio file data in request.response 233 | loader.context.decodeAudioData( 234 | request.response, 235 | function(buffer) { 236 | if (!buffer) { 237 | alert('error decoding file data: ' + url); 238 | return; 239 | } 240 | loader.bufferList[index] = buffer; 241 | if (++loader.loadCount == loader.urlList.length) { 242 | loader.onload(loader.bufferList); 243 | } 244 | }, 245 | function(error) { 246 | console.error('decodeAudioData error', error); 247 | } 248 | ); 249 | }; 250 | 251 | request.onerror = function() { 252 | alert('BufferLoader: XHR error'); 253 | }; 254 | 255 | request.send(); 256 | }; 257 | 258 | BufferLoader.prototype.load = function() { 259 | for (var i = 0; i < this.urlList.length; ++i) { 260 | this.loadBuffer(this.urlList[i], i); 261 | } 262 | }; 263 | -------------------------------------------------------------------------------- /scripts/ship.js: -------------------------------------------------------------------------------- 1 | function Ship(containerElement, fieldWidth, fieldHeight) { 2 | var self = this; 3 | 4 | // Constants 5 | var A = 50; // acceleration factor 6 | var D = 200; // deceleration factor 7 | var AA = 100; // angular acceleration factor 8 | var AD = 130; // angular deceleration factor 9 | var MAX_V = 500; // maximum permitted linear velocity 10 | var MAX_AV = 200; // maximum permitted angular acceleration 11 | // field boundary limits 12 | var MIN_X = -fieldWidth * 0.4; 13 | var MAX_X = fieldWidth * 0.4; 14 | var MAX_Y = fieldHeight * 0.4; 15 | var MIN_Y = -fieldHeight * 0.4; 16 | 17 | self.el = containerElement; 18 | self.lastTimestamp = null; 19 | 20 | // linear position 21 | self.x = 0; 22 | self.y = 100; 23 | self.z = 0; 24 | 25 | // linear velocity 26 | self.vx = 0; 27 | self.vy = 0; 28 | 29 | // rotational position 30 | self.rx = 90; 31 | self.ry = 0; 32 | self.rz = 0; 33 | 34 | // rotational velocity 35 | self.vrx = 0; 36 | self.vry = 0; 37 | self.vrz = 0; 38 | 39 | self.moveLeft = function () { 40 | self.vx += A; 41 | self.vry += AA; 42 | self.vrz += AA/2; 43 | }; 44 | 45 | self.moveRight = function () { 46 | self.vx-= A; 47 | self.vry -= AA; 48 | self.vrz -= AA/2; 49 | }; 50 | 51 | self.moveUp = function () { 52 | self.vy -= A; 53 | self.vrx -= AA/1.3; 54 | }; 55 | 56 | self.moveDown = function () { 57 | self.vy += A; 58 | self.vrx += AA/1.3; 59 | }; 60 | 61 | self.updatePosition = function(timestamp) { 62 | var step; 63 | if (self.lastTimestamp === null || 64 | 100 < timestamp - self.lastTimestamp) { 65 | self.lastTimestamp = timestamp; 66 | } 67 | step = (timestamp - self.lastTimestamp) / 1000; 68 | self.lastTimestamp = timestamp; 69 | 70 | enforceFieldBoundary(); 71 | 72 | self.x += self.vx * step; 73 | self.y += self.vy * step; 74 | self.ry += self.vry * step; 75 | self.rz += self.vrz * step; 76 | self.rx += self.vrx * step; 77 | 78 | self.vx = applyDeceleration(self.vx, D * step); 79 | self.vy = applyDeceleration(self.vy, D * step); 80 | self.vrx = applyRotationalDeceleration(self.vrx, self.rx, 90, AD * step); 81 | self.vry = applyRotationalDeceleration(self.vry, self.ry, 0, AD * step); 82 | self.vrz = applyRotationalDeceleration(self.vrz, self.rz, 0, AD * step); 83 | 84 | self.el.style.transform = 85 | 'translateZ(' + self.z + 'px) ' + 86 | 'translateX(' + self.x + 'px) ' + 87 | 'translateY(' + self.y + 'px) ' + 88 | 'rotateX(' + self.rx + 'deg) ' + 89 | 'rotateY(' + self.ry + 'deg) ' + 90 | 'rotateZ(' + self.rz + 'deg) '; 91 | }; 92 | 93 | function enforceFieldBoundary() { 94 | var bounceFactor = 0.5; 95 | var delta; 96 | if (MAX_X < self.x) { 97 | delta = self.x - MAX_X; 98 | self.vx -= delta * bounceFactor; 99 | } 100 | if (self.x < MIN_X) { 101 | delta = MIN_X - self.x; 102 | self.vx += delta * bounceFactor; 103 | } 104 | if (MAX_Y < self.y) { 105 | delta = self.y - MAX_Y; 106 | self.vy -= delta * bounceFactor; 107 | } 108 | if (self.y < MIN_Y) { 109 | delta = MIN_Y - self.y; 110 | self.vy += delta * bounceFactor; 111 | } 112 | 113 | } 114 | 115 | function applyDeceleration(oldValue, decelerationFactor) { 116 | var newValue; 117 | 118 | if (0 < oldValue) { 119 | newValue = oldValue - decelerationFactor; 120 | } else if (oldValue < 0) { 121 | newValue = oldValue + decelerationFactor; 122 | } else { 123 | newValue = oldValue; 124 | } 125 | 126 | if (Math.abs(oldValue) < decelerationFactor) { 127 | newValue = 0; 128 | } 129 | 130 | if (MAX_V < newValue) { 131 | newValue = MAX_V; 132 | } 133 | if (newValue < -MAX_V) { 134 | newValue = -MAX_V; 135 | } 136 | 137 | return newValue; 138 | } 139 | 140 | function applyRotationalDeceleration(oldValue, currentAngle, targetAngle, decelerationFactor) { 141 | var newValue; 142 | 143 | var delta = currentAngle - targetAngle; 144 | if (0 < delta) { 145 | newValue = -delta * decelerationFactor; 146 | } else if (delta < 0) { 147 | newValue = -delta * decelerationFactor; 148 | } else { 149 | newValue = oldValue; 150 | } 151 | 152 | if (Math.abs(targetAngle - currentAngle) < decelerationFactor) { 153 | newValue = 0; 154 | } 155 | 156 | if (MAX_AV < newValue) { 157 | newValue = MAX_AV; 158 | } 159 | if (newValue < -MAX_AV) { 160 | newValue = -MAX_AV; 161 | } 162 | 163 | return newValue; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /scripts/shot.js: -------------------------------------------------------------------------------- 1 | 2 | function Shot(el, x, y) { 3 | var self = this; 4 | var range = 15000; // how far into the distance before it disappears 5 | var speed = 5000; // distance (in pixels) travelled in 1 second; 6 | self.lastTimestamp = null; 7 | self.el = el; 8 | self.x = x; 9 | self.y = y; 10 | self.z = 0; 11 | self.hit = false; // has the shot collided with an alien? 12 | 13 | /** 14 | * The x and y is the position of the ship, which affects how the shots will be offset 15 | * @param x 16 | * @param y 17 | * @param timestamp 18 | * @returns {boolean} 19 | */ 20 | self.updatePosition = function(x, y, timestamp) { 21 | if (self.lastTimestamp === null || 22 | 100 < timestamp - self.lastTimestamp) { 23 | self.lastTimestamp = timestamp; 24 | } 25 | self.z -= (timestamp - self.lastTimestamp) / 1000 * speed; 26 | self.lastTimestamp = timestamp; 27 | var offsetX = self.x - x; 28 | var offsetY = self.y - y; 29 | var opacity = (range + self.z) / range; 30 | 31 | self.el.style.transform = 32 | 'translateY(' + (self.y + offsetY) + 'px) ' + 33 | 'translateX(' + (self.x + offsetX) + 'px) ' + 34 | 'translateZ(' + self.z + 'px) ' + 35 | 'rotateX(90deg)'; 36 | self.el.style.opacity = opacity; 37 | return self.z < -range || self.hit; 38 | }; 39 | } 40 | 41 | var shotFactory = (function() { 42 | var MAX_FIREPOWER = 10; 43 | var FIREPOWER_GAIN_PER_SECOND = 4; 44 | 45 | var shotElement; 46 | var shots = []; 47 | var firepower = MAX_FIREPOWER; 48 | var lastTimestamp = null; 49 | 50 | return { 51 | setTemplate: function(el) { 52 | shotElement = el.cloneNode(false); 53 | shotElement.style.display = 'block'; 54 | }, 55 | create: function(ship) { 56 | if (0 < Math.round(firepower)) { 57 | throttle(function () { 58 | 59 | if (3 < firepower) { 60 | var spread = document.documentElement.clientWidth * 0.03; 61 | var shotL = { 62 | x: ship.x - spread * Math.cos(ship.ry * (Math.PI/180)), 63 | y: ship.y - Math.tan(ship.ry * (Math.PI/180)) * spread 64 | }; 65 | var shotR = { 66 | x: ship.x + spread * Math.cos(ship.ry * (Math.PI/180)), 67 | y: ship.y + Math.tan(ship.ry * (Math.PI/180)) * spread 68 | }; 69 | 70 | var shotLeftElement = shotElement.cloneNode(false); 71 | document.querySelector('.scene').appendChild(shotLeftElement); 72 | shots.push(new Shot(shotLeftElement, shotL.x, shotL.y)); 73 | var shotRightElement = shotElement.cloneNode(false); 74 | document.querySelector('.scene').appendChild(shotRightElement); 75 | shots.push(new Shot(shotRightElement, shotR.x, shotR.y)); 76 | 77 | } else { 78 | var newElement = shotElement.cloneNode(false); 79 | document.querySelector('.scene').appendChild(newElement); 80 | shots.push(new Shot(newElement, ship.x, ship.y)); 81 | } 82 | emitShotEvent(ship.x, ship.y); 83 | firepower --; 84 | 85 | }, 150); 86 | } 87 | }, 88 | updatePositions: function(ship, timestamp) { 89 | var shotsToRemove = []; 90 | var remove, i; 91 | for(i = 0; i < shots.length; i++) { 92 | remove = shots[i].updatePosition(ship.x, ship.y, timestamp); 93 | if (remove) { 94 | shotsToRemove.push(i); 95 | } 96 | } 97 | 98 | // remove any shots that have gone too distant 99 | for(i = shotsToRemove.length - 1; i >= 0; --i) { 100 | var el = shots[shotsToRemove[i]].el; 101 | shots.splice(shotsToRemove[i], 1); 102 | document.querySelector('.scene').removeChild(el); 103 | } 104 | 105 | replenishFirepower(timestamp); 106 | }, 107 | shots: function() { 108 | return shots; 109 | }, 110 | firepower: function() { 111 | return firepower; 112 | } 113 | }; 114 | 115 | function replenishFirepower(timestamp) { 116 | if (lastTimestamp === null || 117 | 100 < timestamp - lastTimestamp) { 118 | lastTimestamp = timestamp; 119 | } 120 | var deltaSeconds = (timestamp - lastTimestamp) / 1000; 121 | 122 | if (firepower < MAX_FIREPOWER) { 123 | firepower += deltaSeconds * FIREPOWER_GAIN_PER_SECOND; 124 | } 125 | 126 | lastTimestamp = timestamp; 127 | } 128 | 129 | function emitShotEvent(x, y) { 130 | var event = new CustomEvent('shot', { 'detail': { firepower: firepower } }); 131 | document.dispatchEvent(event); 132 | } 133 | 134 | })(); 135 | 136 | var timer, canFire = true; 137 | function throttle(fn, delay) { 138 | if (canFire) { 139 | fn(); 140 | canFire = false; 141 | timer = setTimeout(function () { 142 | canFire = true; 143 | }, delay); 144 | } 145 | } -------------------------------------------------------------------------------- /scripts/track.js: -------------------------------------------------------------------------------- 1 | function Track(el) { 2 | var self = this; 3 | self.container = el; 4 | 5 | this.start = function () { 6 | [].forEach.call(self.container.querySelectorAll('.track'), function(track) { 7 | track.style.webkitAnimationPlayState = 'running'; 8 | track.style.animationPlayState = 'running'; 9 | }); 10 | }; 11 | 12 | this.stop = function() { 13 | [].forEach.call(self.container.querySelectorAll('.track'), function(track) { 14 | track.style.webkitAnimationPlayState = 'paused'; 15 | track.style.animationPlayState = 'paused'; 16 | }); 17 | }; 18 | 19 | this.update = function(ship) { 20 | var x = ship.x * -0.3; 21 | var y = ship.y * -0.3; 22 | self.container.style.transform = "translateX(" + x + 'px) translateY(' + y + 'px)'; 23 | }; 24 | 25 | this.stop(); 26 | } -------------------------------------------------------------------------------- /scripts/visualizer.js: -------------------------------------------------------------------------------- 1 | var visualizer = (function() { 2 | var module = {}; 3 | var highsEl, lowsEl; 4 | 5 | module.setElement = function(el) { 6 | highsEl = el.querySelector('.highs'); 7 | lowsEl = el.querySelector('.lows'); 8 | 9 | highsEl.style.opacity = 0; 10 | lowsEl.style.opacity = 0; 11 | }; 12 | 13 | module.start = function(audioSource) { 14 | 15 | function tick() { 16 | 17 | var frequencyData = audioSource.getAudioData().frequencyData; 18 | 19 | var lows = getAverageValueInRange(frequencyData, 0, 15) / 255; 20 | var highs = getAverageValueInRange(frequencyData, 16, 25) / 255; 21 | 22 | lowsEl.style.opacity = lows; 23 | highsEl.style.opacity = highs; 24 | 25 | setTimeout(tick, 300); 26 | } 27 | 28 | tick(); 29 | }; 30 | 31 | function getAverageValueInRange(frequencyData, start, end) { 32 | var sum = 0; 33 | 34 | for (var i = start; i <= end; i ++) { 35 | sum += frequencyData[i]; 36 | } 37 | 38 | return sum / (end - start); 39 | } 40 | 41 | return module; 42 | })(); -------------------------------------------------------------------------------- /styles/alien.less: -------------------------------------------------------------------------------- 1 | @import "vars"; 2 | 3 | @orange: orange; 4 | @alien-box-shadow: inset 0 0 10px @orange, 0px 0px 10px 0px @orange; 5 | @alien-fill-color: rgba(0,0,0,0.7); 6 | @alien-border-width: @scale * 0.15; 7 | 8 | .alien-box-shadow(@color) { 9 | box-shadow: inset 0 0 10px @color, 0px 0px 10px 0px @color;; 10 | } 11 | 12 | @keyframes rotate { 13 | 0%{ transform: rotateY(0);} 14 | 100%{ transform: rotateY(359deg);} 15 | } 16 | 17 | .alien-container { 18 | display: none; 19 | position: inherit; 20 | z-index: 20; 21 | } 22 | .alien { 23 | border: @alien-border-width solid @orange; 24 | .alien-box-shadow(@orange); 25 | background-color: @alien-fill-color; 26 | transition: border-width 0.7s, opacity 0.5s 0.2s; 27 | } 28 | .red .alien, .red .mouth:before, .red .mouth:after { 29 | border: @alien-border-width solid #ff2e53; 30 | .alien-box-shadow(#ff2e53); 31 | } 32 | .blue .alien, .blue .mouth:before, .blue .mouth:after { 33 | border: @alien-border-width solid #3183ff; 34 | .alien-box-shadow(#3183ff); 35 | } 36 | .green .alien, .green .mouth:before, .green .mouth:after { 37 | border: @alien-border-width solid greenyellow; 38 | .alien-box-shadow(greenyellow); 39 | } 40 | .white .alien, .white .mouth:before, .white .mouth:after { 41 | border: @alien-border-width solid white; 42 | .alien-box-shadow(white); 43 | } 44 | 45 | .alien-container.hit { 46 | 47 | &>.alien { 48 | border-color: white; 49 | border-width: @scale * 2; 50 | opacity: 0; 51 | animation: rotate 0.8s infinite; 52 | } 53 | 54 | .arm-container { 55 | .arm { 56 | border-color: white; 57 | border-width: @scale * 2; 58 | opacity: 0; 59 | &.left { 60 | transform: translateZ(-10px) rotateZ(-120deg) translateY(@scale * 20) rotateX(50deg); 61 | } 62 | &.right { 63 | transform: translateZ(-10px) rotateZ(120deg) translateY(@scale * 20) rotateX(50deg); 64 | } 65 | &.bottom { 66 | transform: translateZ(-10px) translateY(g@scale * 20) rotateX(50deg); 67 | } 68 | } 69 | } 70 | } 71 | 72 | @body-width: 7 * @scale; 73 | @body-height: 7 * @scale; 74 | .body { 75 | width: @body-width; 76 | height: @body-height; 77 | margin: -@body-height/2 -@body-width/2; 78 | } 79 | 80 | 81 | @eye-width: 2 * @scale; 82 | @eye-height: 0.5 * @scale; 83 | .eye { 84 | width: @eye-width; 85 | height: @eye-height; 86 | margin-top: -@eye-height; 87 | &.left { 88 | margin-left: -(@body-width - @eye-width) / 2; 89 | transform: rotateZ(45deg); 90 | } 91 | 92 | &.right { 93 | margin-left: (@body-width/2 - @eye-width) / 2; 94 | transform: rotateZ(-45deg); 95 | } 96 | } 97 | 98 | @mouth-width: @body-width * 0.1; 99 | @mouth-height: @body-height * 0.1; 100 | .mouth { 101 | width: @mouth-width; 102 | height: @mouth-height; 103 | margin-left: -(@mouth-width - 2*@alien-border-width)/2; 104 | margin-top: @mouth-height * 2; 105 | animation: mouth-animation 0.5s 0.2s infinite alternate; 106 | 107 | &:before { 108 | content: ''; 109 | position: absolute; 110 | width: @mouth-width; 111 | height: @mouth-height; 112 | margin-top: @mouth-height; 113 | margin-left: -(@mouth-width + 2 * @alien-border-width) * 1.5; 114 | animation: mouth-animation 0.5s 0s infinite alternate; 115 | .alien(); 116 | } 117 | &:after { 118 | content: ''; 119 | position: absolute; 120 | width: @mouth-width; 121 | height: @mouth-height; 122 | margin-top: @mouth-height; 123 | margin-left: (@mouth-width + 1 * @alien-border-width) * 1.5; 124 | animation: mouth-animation 0.5s 0.4s infinite alternate; 125 | .alien(); 126 | } 127 | } 128 | 129 | @keyframes spin { 130 | 0% { transform: rotateZ(0); } 131 | 100% { transform: rotateZ(359deg); } 132 | } 133 | 134 | .arm-container { 135 | animation: spin 8s linear infinite; 136 | } 137 | 138 | @arm-width: 0.5 * @scale; 139 | @arm-height: 15 * @scale; 140 | .arm { 141 | width: @arm-width; 142 | height: @arm-height; 143 | transition: transform 1.5s, border-width 0.7s, opacity 0.5s 0.2s; 144 | 145 | &.left { 146 | transform-origin: top center; 147 | transform: translateZ(-10px) rotateZ(-120deg) translateY(@scale * 2) rotateX(-50deg); 148 | } 149 | &.right { 150 | transform-origin: top center; 151 | transform: translateZ(-10px) rotateZ(120deg) translateY(@scale * 2) rotateX(-50deg); 152 | } 153 | &.bottom { 154 | transform-origin: top center; 155 | transform: translateZ(-10px) translateY(@scale * 2) rotateX(-50deg); 156 | } 157 | } -------------------------------------------------------------------------------- /styles/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | margin: 0; 5 | box-sizing: border-box; 6 | background-color: black; 7 | } 8 | a:link, 9 | a:visited { 10 | text-decoration: none; 11 | color: lightgreen; 12 | } 13 | a:hover, 14 | a:active { 15 | color: green; 16 | } 17 | .browser-warning { 18 | font-family: "Helvetic Neue", Helvetica, Arial, sans-serif; 19 | padding: 10px; 20 | position: absolute; 21 | top: 0; 22 | text-align: center; 23 | background-color: yellow; 24 | width: 100%; 25 | color: black; 26 | opacity: 0.8; 27 | } 28 | .browser-warning a:link, 29 | .browser-warning a:visited { 30 | color: blue; 31 | } 32 | .loader { 33 | position: absolute; 34 | overflow: hidden; 35 | top: 0; 36 | left: 0; 37 | bottom: 0; 38 | right: 0; 39 | text-align: center; 40 | color: white; 41 | text-transform: uppercase; 42 | font-family: 'Exo', sans-serif; 43 | font-weight: 900; 44 | font-style: italic; 45 | background-color: black; 46 | color: #a1a1a1; 47 | padding-top: 25vh; 48 | transition: opacity 1s, visibility 0s 1.5s; 49 | } 50 | .loader h1 { 51 | text-transform: uppercase; 52 | font-size: 10vw; 53 | } 54 | .loader.hidden { 55 | visibility: hidden; 56 | opacity: 0; 57 | } 58 | .title-screen-container { 59 | position: absolute; 60 | overflow: hidden; 61 | top: 0; 62 | left: 0; 63 | bottom: 0; 64 | right: 0; 65 | text-align: center; 66 | color: white; 67 | text-transform: uppercase; 68 | font-family: 'Exo', sans-serif; 69 | font-weight: 900; 70 | font-style: italic; 71 | transition: transform 2s cubic-bezier(0.88, 0.03, 0.89, 0.69), opacity 1s 1s, visibility 0s 2s; 72 | } 73 | .title-screen-container.hidden { 74 | transform: translate3d(0px, 200px, 500px) rotateX(90deg); 75 | opacity: 0; 76 | visibility: hidden; 77 | } 78 | .title-screen-container .gh-star-container { 79 | text-align: right; 80 | margin-top: 20px; 81 | opacity: 0.6; 82 | } 83 | .title-screen-container .title-container { 84 | margin: 10vh auto; 85 | width: 80vw; 86 | font-size: 13vw; 87 | color: black; 88 | line-height: 10vw; 89 | text-shadow: -1px -1px 10px #008000, 1px -1px 0 #008000, -1px 1px 0 #008000, 1px 1px 10px #008000; 90 | } 91 | .title-screen-container .title-container .line1 { 92 | margin-left: -17vw; 93 | font-size: 8vw; 94 | line-height: 6vw; 95 | } 96 | .title-screen-container .title-container .line2 { 97 | margin-left: 0vw; 98 | } 99 | .title-screen-container .title-container .line3 { 100 | margin-left: 0; 101 | } 102 | .title-screen-container .info-container { 103 | color: white; 104 | width: 30vw; 105 | height: 8vh; 106 | overflow: hidden; 107 | transition: all 0.5s; 108 | position: absolute; 109 | top: 67vh; 110 | } 111 | .title-screen-container .info-container.instructions { 112 | left: 10vw; 113 | } 114 | .title-screen-container .info-container.about { 115 | right: 10vw; 116 | } 117 | .title-screen-container .info-container.display { 118 | z-index: 10; 119 | width: 100vw; 120 | height: 100vh; 121 | background-color: rgba(0, 0, 0, 0.9); 122 | top: 0; 123 | } 124 | .title-screen-container .info-container.display.instructions { 125 | left: 0; 126 | } 127 | .title-screen-container .info-container.display.about { 128 | right: 0; 129 | } 130 | .title-screen-container .info-container.display .info-body { 131 | opacity: 1; 132 | transition: opacity 0.5s 0.5s; 133 | } 134 | .title-screen-container .info-container.display .open-info-container { 135 | font-size: 4vw; 136 | line-height: 18vh; 137 | } 138 | .title-screen-container .info-container:not(.display):hover { 139 | background-color: rgba(255, 255, 255, 0.25); 140 | } 141 | .title-screen-container .info-container .info-body { 142 | max-width: 500px; 143 | margin: auto; 144 | opacity: 0; 145 | transition: opacity 0.2s; 146 | font-family: verdana, arial, helvetica, sans serif; 147 | text-transform: none; 148 | font-style: normal; 149 | font-weight: 400; 150 | color: #ddd; 151 | } 152 | .title-screen-container .info-container .open-info-container { 153 | cursor: pointer; 154 | transition: font-size 0.5s, line-height 0.5s; 155 | font-size: 3vw; 156 | line-height: 8vh; 157 | } 158 | .title-screen-container .info-container .close-info-container { 159 | font-family: 'Exo', sans-serif; 160 | font-weight: 900; 161 | font-style: italic; 162 | cursor: pointer; 163 | } 164 | .title-screen-container .info-container .key { 165 | padding: 3px 8px; 166 | background-color: #808080; 167 | border-radius: 3px; 168 | border: 1px solid #999; 169 | display: inline-block; 170 | font-family: Consolas, "Lucida Console", Monaco, "Courier New", monospace; 171 | } 172 | .start-sign, 173 | .replay-sign { 174 | position: absolute; 175 | bottom: 1vh; 176 | width: 100%; 177 | font-size: 4vw; 178 | animation: flash 1s infinite; 179 | } 180 | @keyframes flash { 181 | 0% { 182 | opacity: 0; 183 | } 184 | 100% { 185 | opacity: 0.7; 186 | } 187 | } 188 | .game-over-container { 189 | position: absolute; 190 | overflow: hidden; 191 | top: 0; 192 | left: 0; 193 | bottom: 0; 194 | right: 0; 195 | text-align: center; 196 | color: white; 197 | text-transform: uppercase; 198 | font-family: 'Exo', sans-serif; 199 | font-weight: 900; 200 | font-style: italic; 201 | font-size: 2em; 202 | text-shadow: -1px -1px 10px #008000, 1px -1px 0 #008000, -1px 1px 0 #008000, 1px 1px 10px #008000; 203 | transition: transform 5s cubic-bezier(0.88, 0.03, 0.26, 0.94), opacity 1s 1s, visibility 0s 0s; 204 | } 205 | .game-over-container .game-won.hidden { 206 | display: none; 207 | } 208 | .game-over-container .game-lost.hidden { 209 | display: none; 210 | } 211 | .game-over-container h1 { 212 | font-size: 6vw; 213 | } 214 | .game-over-container h2 { 215 | font-size: 4vw; 216 | } 217 | .game-over-container .score-card { 218 | border: 1px solid #0f3d0f; 219 | background-color: rgba(0, 0, 0, 0.34); 220 | max-width: 400px; 221 | margin: auto; 222 | } 223 | .game-over-container .score-card ul { 224 | list-style-type: none; 225 | padding: 0; 226 | } 227 | .game-over-container .new-record { 228 | display: none; 229 | color: #a0a000; 230 | font-size: 0.5em; 231 | margin-left: 10px; 232 | } 233 | .game-over-container .record { 234 | color: #aaa; 235 | font-size: 0.7em; 236 | } 237 | .game-over-container.hidden { 238 | opacity: 0; 239 | visibility: hidden; 240 | } 241 | #player { 242 | display: none; 243 | } 244 | .scene { 245 | position: absolute; 246 | top: 0; 247 | left: 0; 248 | right: 0; 249 | bottom: 0; 250 | perspective: 800px; 251 | transform-origin: center center; 252 | overflow: hidden; 253 | } 254 | .face { 255 | position: absolute; 256 | top: 50%; 257 | left: 50%; 258 | transform-style: preserve-3d; 259 | } 260 | .overlay { 261 | position: absolute; 262 | overflow: hidden; 263 | perspective: 800px; 264 | top: 0; 265 | left: 0; 266 | bottom: 0; 267 | right: 0; 268 | } 269 | .firepower-meter-container { 270 | position: absolute; 271 | opacity: 0.5; 272 | left: 20px; 273 | bottom: 20px; 274 | z-index: 300; 275 | overflow: hidden; 276 | width: 300px; 277 | height: 30px; 278 | transition: transform 0.5s 4s; 279 | } 280 | .firepower-meter-container.hidden { 281 | transform: translateX(-500px); 282 | } 283 | .firepower-meter-container .firepower-meter { 284 | display: block; 285 | position: relative; 286 | border-radius: 5px; 287 | width: 300px; 288 | height: 30px; 289 | background: linear-gradient(to right, #001364 0%, #000b96 21%, #0098ff 60%, #ffffff 87%); 290 | /* W3C */ 291 | } 292 | .firepower-meter-container .firepower-meter span { 293 | position: absolute; 294 | font-family: 'Exo', sans-serif; 295 | font-weight: 900; 296 | font-style: italic; 297 | text-transform: uppercase; 298 | color: rgba(255, 255, 255, 0.62); 299 | font-size: 27px; 300 | top: -2px; 301 | left: 10px; 302 | } 303 | .announcement { 304 | opacity: 0; 305 | visibility: hidden; 306 | position: absolute; 307 | width: 100%; 308 | margin-top: 40vh; 309 | font-family: 'Exo', sans-serif; 310 | font-weight: 900; 311 | font-style: italic; 312 | color: white; 313 | text-align: center; 314 | text-transform: uppercase; 315 | transition: opacity 0.5s, visibility 0s; 316 | } 317 | .announcement.visible { 318 | opacity: 0.8; 319 | visibility: visible; 320 | } 321 | .announcement .title { 322 | font-size: 12vh; 323 | } 324 | .announcement .subtitle { 325 | font-size: 8vh; 326 | } 327 | .score-container { 328 | position: absolute; 329 | bottom: 10px; 330 | right: 10px; 331 | transition: transform 0.5s 4s; 332 | } 333 | .score-container.hidden { 334 | transform: translateX(500px); 335 | } 336 | .score-container .score { 337 | color: white; 338 | font-size: 8vh; 339 | font-family: 'Exo', sans-serif; 340 | font-weight: 900; 341 | } 342 | .lives-container { 343 | position: absolute; 344 | bottom: 80px; 345 | right: 15px; 346 | transition: transform 0.5s 4.2s; 347 | } 348 | .lives-container.hidden { 349 | transform: translateX(500px); 350 | } 351 | .lives-container .life { 352 | font-size: 45px; 353 | color: white; 354 | display: inline-block; 355 | opacity: 0.7; 356 | transition: opacity 1s; 357 | } 358 | .lives-container .life.hidden { 359 | opacity: 0; 360 | } 361 | @keyframes spin { 362 | 0% { 363 | transform: rotateZ(0); 364 | } 365 | 100% { 366 | transform: rotateZ(359); 367 | } 368 | } 369 | .background { 370 | animation: spin 600s linear infinite; 371 | } 372 | @keyframes stars-animation-1 { 373 | 0% { 374 | transform: rotateZ(90deg) translateZ(-300px); 375 | } 376 | 100% { 377 | transform: rotateZ(90deg) translateZ(200px); 378 | } 379 | } 380 | @keyframes stars-animation-2 { 381 | 0% { 382 | transform: rotateZ(180deg) translateZ(0px); 383 | } 384 | 100% { 385 | transform: rotateZ(180deg) translateZ(800px); 386 | } 387 | } 388 | .stars { 389 | position: inherit; 390 | width: 1px; 391 | height: 1px; 392 | animation: stars-animation-1 60s linear infinite alternate; 393 | box-shadow: 3vw 1vh #ffdad0, 6vw 49vh #ffd6e7, 15vw -33vh #ffffff, 17vw -5vh #ffffff, 20vw 10vh #ffffff, 22vw 15vh #bbc3ff, 26vw 23vh #ffffff, 27vw -26vh #ffffff, 33vw 17vh #f1ffad, 38vw -2vh #ffd2a3, 41vw -9vh #ffffff, -2vw 2vh #ffffff, -24vw 45vh #dbffeb, -6vw 20vh #ffffff, -11vw 34vh #fff6c8, -18vw -40vh #ffffff, -33vw -23vh #b9cbff, -4vw 20vh #ffffff, -1vw 30vh #b0ffff; 394 | } 395 | .stars:before { 396 | position: absolute; 397 | content: ''; 398 | width: 2px; 399 | height: 2px; 400 | box-shadow: 3vw 1vh #ffdad0, 6vw 49vh #ffd6e7, 15vw -33vh #ffffff, 17vw -5vh #ffffff, 20vw 10vh #ffffff, 22vw 15vh #bbc3ff, 26vw 23vh #ffffff, 27vw -26vh #ffffff, 33vw 17vh #f1ffad, 38vw -2vh #ffd2a3, 41vw -9vh #ffffff, -2vw 2vh #ffffff, -24vw 45vh #dbffeb, -6vw 20vh #ffffff, -11vw 34vh #fff6c8, -18vw -40vh #ffffff, -33vw -23vh #b9cbff, -4vw 20vh #ffffff, -1vw 30vh #b0ffff; 401 | transform: rotateZ(90deg); 402 | } 403 | .stars:after { 404 | position: absolute; 405 | content: ''; 406 | width: 1px; 407 | height: 1px; 408 | animation: stars-animation-2 60s linear infinite alternate; 409 | box-shadow: 3vw 1vh #ffdad0, 6vw 49vh #ffd6e7, 15vw -33vh #ffffff, 17vw -5vh #ffffff, 20vw 10vh #ffffff, 22vw 15vh #bbc3ff, 26vw 23vh #ffffff, 27vw -26vh #ffffff, 33vw 17vh #f1ffad, 38vw -2vh #ffd2a3, 41vw -9vh #ffffff, -2vw 2vh #ffffff, -24vw 45vh #dbffeb, -6vw 20vh #ffffff, -11vw 34vh #fff6c8, -18vw -40vh #ffffff, -33vw -23vh #b9cbff, -4vw 20vh #ffffff, -1vw 30vh #b0ffff; 410 | transform: rotateZ(180deg); 411 | } 412 | @keyframes planet-animation { 413 | 0% { 414 | transform: translateZ(0px); 415 | } 416 | 100% { 417 | transform: translateZ(300px); 418 | } 419 | } 420 | .planet { 421 | position: absolute; 422 | z-index: 1; 423 | top: -25vw; 424 | left: -75vw; 425 | width: 50vw; 426 | height: 50vw; 427 | border-radius: 50%; 428 | background-color: #111; 429 | animation: planet-animation 60s linear infinite alternate; 430 | box-shadow: inset 0 0 50px #ffffff, inset 20px 0 80px #ff00ff, inset -20px 0 80px #00ffff, inset 20px 0 300px #ff00ff, inset -20px 0 300px #00ffff, 0 0 50px #ffffff, -10px 0 80px #ff00ff, 10px 0 80px #00ffff; 431 | } 432 | /** 433 | * The ship 434 | */ 435 | .ship-container { 436 | position: inherit; 437 | z-index: 50; 438 | } 439 | .ship { 440 | border: 0.15vw solid #0000ff; 441 | box-shadow: inset 0 0 10px #0000ff, 0px 0px 10px 0px #0000ff; 442 | } 443 | .hull { 444 | width: 2.5vw; 445 | height: 10vw; 446 | margin: -5vw -1.25vw; 447 | background-color: rgba(0, 0, 0, 0.5); 448 | } 449 | .hull.top { 450 | transform-origin: top center; 451 | transform: rotateX(5deg); 452 | } 453 | .hull.bottom { 454 | transform-origin: top center; 455 | transform: rotateX(-5deg); 456 | } 457 | .hull.back { 458 | margin: 4.1127326vw -1.25vw; 459 | height: 1.73648178vw; 460 | transform-origin: center center; 461 | transform: rotateX(90deg); 462 | } 463 | /* 464 | // does not work for some reason 465 | .generate-booster-animation(@name, @color) { 466 | .keyframe(@n, @i: 0) when (@i =< @n) { 467 | @percentage: percentage(@i/10); 468 | @{percentage} { 469 | @random: `Math.random()`; 470 | @fade: percentage(@random); 471 | background-color: fade(@color, @fade); 472 | width: @fade; 473 | } 474 | .keyframe(@n, (@i + 1)); 475 | } 476 | 477 | @keyframes @name { 478 | .keyframe(10); 479 | } 480 | } 481 | .generate-booster-animation(booster-amination-1, white); 482 | .generate-booster-animation(booster-amination-2, red); 483 | .generate-booster-animation(booster-amination-3, orange);*/ 484 | .booster { 485 | width: 1.5vw; 486 | height: 1.5vw; 487 | margin: 4.23097349vw -0.75vw; 488 | border-radius: 50% 50%; 489 | transform: rotateX(90deg); 490 | animation: booster-animation-1 10s infinite; 491 | } 492 | .booster:before { 493 | content: ''; 494 | position: absolute; 495 | display: inline-block; 496 | width: 1.35vw; 497 | height: 1.35vw; 498 | margin: -0.075vw; 499 | border-radius: 50% 50%; 500 | transform: translateZ(0.4vw); 501 | } 502 | .booster:after { 503 | content: ''; 504 | position: absolute; 505 | display: inline-block; 506 | width: 1.65vw; 507 | height: 1.65vw; 508 | margin: -0.225vw; 509 | border-radius: 50% 50%; 510 | border: 0.15vw solid #9999ff; 511 | transform: translateZ(-0.4vw); 512 | } 513 | .wing { 514 | width: 20vw; 515 | height: 5vw; 516 | margin: 0 -10vw; 517 | border-radius: 50% 50% 0 0; 518 | border-width: 0.3vw; 519 | border-bottom-width: 0.15vw; 520 | background-color: rgba(0, 0, 0, 0.5); 521 | } 522 | .wing.top { 523 | transform-origin: top center; 524 | transform: rotateX(5deg); 525 | } 526 | .wing.bottom { 527 | transform-origin: top center; 528 | transform: rotateX(-5deg); 529 | } 530 | .wing.back { 531 | border-width: 0.15vw; 532 | margin: 4.69155986vw -10vw; 533 | height: 0.86824089vw; 534 | transform-origin: center center; 535 | transform: rotateX(90deg); 536 | border-radius: 0; 537 | } 538 | .gun { 539 | width: 0.1vw; 540 | height: 2vw; 541 | } 542 | .gun.right { 543 | margin: -1vw 5vw; 544 | } 545 | .gun.left { 546 | margin: -1vw -5vw; 547 | } 548 | /** 549 | * The track 550 | */ 551 | .track-container { 552 | position: inherit; 553 | z-index: 10; 554 | transform-style: preserve-3d; 555 | } 556 | .track { 557 | opacity: 0; 558 | width: 60vw; 559 | height: 600px; 560 | margin: -300px -30vw; 561 | border: 8px solid #008000; 562 | box-shadow: inset 0 0 30px #008000, 0px 0px 30px 0px #008000; 563 | border-left-width: 2.66666667px; 564 | border-right-width: 2.66666667px; 565 | } 566 | .track:before { 567 | content: ''; 568 | position: absolute; 569 | width: 20vw; 570 | height: 600px; 571 | border-left: 2.66666667px solid #008000; 572 | border-right: 2.66666667px solid #008000; 573 | box-shadow: inset 0 0 30px #008000, 0px 0px 30px 0px #008000; 574 | left: 20vw; 575 | } 576 | .track:after { 577 | content: ''; 578 | position: absolute; 579 | width: 60vw; 580 | height: 200px; 581 | border-top: 8px solid #008000; 582 | border-bottom: 8px solid #008000; 583 | box-shadow: inset 0 0 30px #008000, 0px 0px 30px 0px #008000; 584 | top: 200px; 585 | } 586 | @keyframes track-animation-top { 587 | 0% { 588 | opacity: 0; 589 | transform: translateY(-15vw) translateZ(-2000px) rotateX(90deg); 590 | } 591 | 30% { 592 | opacity: 1; 593 | } 594 | 100% { 595 | opacity: 1; 596 | transform: translateY(-15vw) translateZ(1000px) rotateX(90deg); 597 | } 598 | } 599 | @keyframes track-animation-bottom { 600 | 0% { 601 | opacity: 0; 602 | transform: translateY(15vw) translateZ(-2000px) rotateX(90deg); 603 | } 604 | 30% { 605 | opacity: 1; 606 | } 607 | 100% { 608 | opacity: 1; 609 | transform: translateY(15vw) translateZ(1000px) rotateX(90deg); 610 | } 611 | } 612 | .track.top:nth-child(1) { 613 | animation: track-animation-top 3s linear 0s infinite normal; 614 | } 615 | .track.bottom:nth-child(1) { 616 | animation: track-animation-bottom 3s linear 0s infinite normal; 617 | } 618 | .track.top:nth-child(2) { 619 | animation: track-animation-top 3s linear 0.6s infinite normal; 620 | } 621 | .track.bottom:nth-child(2) { 622 | animation: track-animation-bottom 3s linear 0.6s infinite normal; 623 | } 624 | .track.top:nth-child(3) { 625 | animation: track-animation-top 3s linear 1.2s infinite normal; 626 | } 627 | .track.bottom:nth-child(3) { 628 | animation: track-animation-bottom 3s linear 1.2s infinite normal; 629 | } 630 | .track.top:nth-child(4) { 631 | animation: track-animation-top 3s linear 1.8s infinite normal; 632 | } 633 | .track.bottom:nth-child(4) { 634 | animation: track-animation-bottom 3s linear 1.8s infinite normal; 635 | } 636 | .track.top:nth-child(5) { 637 | animation: track-animation-top 3s linear 2.4s infinite normal; 638 | } 639 | .track.bottom:nth-child(5) { 640 | animation: track-animation-bottom 3s linear 2.4s infinite normal; 641 | } 642 | .track.top:nth-child(6) { 643 | animation: track-animation-top 3s linear 3s infinite normal; 644 | } 645 | .track.bottom:nth-child(6) { 646 | animation: track-animation-bottom 3s linear 3s infinite normal; 647 | } 648 | .shot { 649 | display: none; 650 | position: absolute; 651 | z-index: 25; 652 | width: 1vw; 653 | height: 10vw; 654 | margin-top: -5vw; 655 | border-radius: 50%; 656 | background-color: #ffffff; 657 | border: 1vw solid rgba(11, 0, 255, 0.5); 658 | /*box-shadow: 659 | 0 0 30px 15px #fff, */ 660 | /* inner white */ 661 | /* 662 | 0 0 50px 30px rgba(255, 0, 255, 0.62), */ 663 | /* middle magenta */ 664 | /* 665 | 0 0 70px 45px rgba(0, 255, 255, 0.53); */ 666 | /* outer cyan */ 667 | } 668 | .shot:after { 669 | position: absolute; 670 | content: ''; 671 | width: 2vw; 672 | height: 2vw; 673 | background-color: rgba(156, 148, 255, 0.5); 674 | border-radius: 50%; 675 | margin-left: -0.5vw; 676 | transform: rotateX(90deg); 677 | } 678 | @keyframes rotate { 679 | 0% { 680 | transform: rotateY(0); 681 | } 682 | 100% { 683 | transform: rotateY(359deg); 684 | } 685 | } 686 | .alien-container { 687 | display: none; 688 | position: inherit; 689 | z-index: 20; 690 | } 691 | .alien { 692 | border: 0.15vw solid #ffa500; 693 | box-shadow: inset 0 0 10px #ffa500, 0px 0px 10px 0px #ffa500; 694 | background-color: rgba(0, 0, 0, 0.7); 695 | transition: border-width 0.7s, opacity 0.5s 0.2s; 696 | } 697 | .red .alien, 698 | .red .mouth:before, 699 | .red .mouth:after { 700 | border: 0.15vw solid #ff2e53; 701 | box-shadow: inset 0 0 10px #ff2e53, 0px 0px 10px 0px #ff2e53; 702 | } 703 | .blue .alien, 704 | .blue .mouth:before, 705 | .blue .mouth:after { 706 | border: 0.15vw solid #3183ff; 707 | box-shadow: inset 0 0 10px #3183ff, 0px 0px 10px 0px #3183ff; 708 | } 709 | .green .alien, 710 | .green .mouth:before, 711 | .green .mouth:after { 712 | border: 0.15vw solid #adff2f; 713 | box-shadow: inset 0 0 10px #adff2f, 0px 0px 10px 0px #adff2f; 714 | } 715 | .white .alien, 716 | .white .mouth:before, 717 | .white .mouth:after { 718 | border: 0.15vw solid #ffffff; 719 | box-shadow: inset 0 0 10px #ffffff, 0px 0px 10px 0px #ffffff; 720 | } 721 | .alien-container.hit > .alien { 722 | border-color: white; 723 | border-width: 2vw; 724 | opacity: 0; 725 | animation: rotate 0.8s infinite; 726 | } 727 | .alien-container.hit .arm-container .arm { 728 | border-color: white; 729 | border-width: 2vw; 730 | opacity: 0; 731 | } 732 | .alien-container.hit .arm-container .arm.left { 733 | transform: translateZ(-10px) rotateZ(-120deg) translateY(20vw) rotateX(50deg); 734 | } 735 | .alien-container.hit .arm-container .arm.right { 736 | transform: translateZ(-10px) rotateZ(120deg) translateY(20vw) rotateX(50deg); 737 | } 738 | .alien-container.hit .arm-container .arm.bottom { 739 | transform: translateZ(-10px) translateY(g 20vw) rotateX(50deg); 740 | } 741 | .body { 742 | width: 7vw; 743 | height: 7vw; 744 | margin: -3.5vw -3.5vw; 745 | } 746 | .eye { 747 | width: 2vw; 748 | height: 0.5vw; 749 | margin-top: -0.5vw; 750 | } 751 | .eye.left { 752 | margin-left: -2.5vw; 753 | transform: rotateZ(45deg); 754 | } 755 | .eye.right { 756 | margin-left: 0.75vw; 757 | transform: rotateZ(-45deg); 758 | } 759 | .mouth { 760 | width: 0.7vw; 761 | height: 0.7vw; 762 | margin-left: -0.2vw; 763 | margin-top: 1.4vw; 764 | animation: mouth-animation 0.5s 0.2s infinite alternate; 765 | } 766 | .mouth:before { 767 | content: ''; 768 | position: absolute; 769 | width: 0.7vw; 770 | height: 0.7vw; 771 | margin-top: 0.7vw; 772 | margin-left: -1.5vw; 773 | animation: mouth-animation 0.5s 0s infinite alternate; 774 | border: 0.15vw solid #ffa500; 775 | box-shadow: inset 0 0 10px #ffa500, 0px 0px 10px 0px #ffa500; 776 | background-color: rgba(0, 0, 0, 0.7); 777 | transition: border-width 0.7s, opacity 0.5s 0.2s; 778 | } 779 | .mouth:after { 780 | content: ''; 781 | position: absolute; 782 | width: 0.7vw; 783 | height: 0.7vw; 784 | margin-top: 0.7vw; 785 | margin-left: 1.275vw; 786 | animation: mouth-animation 0.5s 0.4s infinite alternate; 787 | border: 0.15vw solid #ffa500; 788 | box-shadow: inset 0 0 10px #ffa500, 0px 0px 10px 0px #ffa500; 789 | background-color: rgba(0, 0, 0, 0.7); 790 | transition: border-width 0.7s, opacity 0.5s 0.2s; 791 | } 792 | @keyframes spin { 793 | 0% { 794 | transform: rotateZ(0); 795 | } 796 | 100% { 797 | transform: rotateZ(359deg); 798 | } 799 | } 800 | .arm-container { 801 | animation: spin 8s linear infinite; 802 | } 803 | .arm { 804 | width: 0.5vw; 805 | height: 15vw; 806 | transition: transform 1.5s, border-width 0.7s, opacity 0.5s 0.2s; 807 | } 808 | .arm.left { 809 | transform-origin: top center; 810 | transform: translateZ(-10px) rotateZ(-120deg) translateY(2vw) rotateX(-50deg); 811 | } 812 | .arm.right { 813 | transform-origin: top center; 814 | transform: translateZ(-10px) rotateZ(120deg) translateY(2vw) rotateX(-50deg); 815 | } 816 | .arm.bottom { 817 | transform-origin: top center; 818 | transform: translateZ(-10px) translateY(2vw) rotateX(-50deg); 819 | } 820 | .visualizer { 821 | position: absolute; 822 | top: 0; 823 | left: 0; 824 | right: 0; 825 | bottom: 0; 826 | } 827 | .visualizer .highs { 828 | width: 100%; 829 | background: linear-gradient(to top, rgba(0, 0, 0, 0) 0%, rgba(89, 86, 255, 0.8) 100%); 830 | height: 25vh; 831 | transition: opacity 0.1s linear; 832 | } 833 | .visualizer .lows { 834 | position: absolute; 835 | width: 100%; 836 | bottom: 0; 837 | background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(89, 86, 255, 0.8) 100%); 838 | height: 25vh; 839 | transition: opacity 0.1s linear; 840 | } 841 | -------------------------------------------------------------------------------- /styles/app.less: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | box-sizing: border-box; 5 | background-color: black; 6 | } 7 | 8 | a:link, a:visited { 9 | text-decoration: none; 10 | color: lightgreen; 11 | } 12 | a:hover, a:active { 13 | color: green; 14 | } 15 | 16 | .browser-warning { 17 | font-family: "Helvetic Neue", Helvetica, Arial, sans-serif; 18 | padding: 10px; 19 | position: absolute; 20 | top: 0; 21 | text-align: center; 22 | background-color: yellow; 23 | width: 100%; 24 | color: black; 25 | opacity: 0.8; 26 | a:link, a:visited { 27 | color: blue; 28 | } 29 | } 30 | 31 | .title-screen-base() { 32 | position: absolute; 33 | overflow: hidden; 34 | top: 0; 35 | left: 0; 36 | bottom: 0; 37 | right: 0; 38 | text-align: center; 39 | color: white; 40 | text-transform: uppercase; 41 | .title-font(); 42 | } 43 | 44 | .green-text-stroke() { 45 | text-shadow: 46 | -1px -1px 10px green, 47 | 1px -1px 0 green, 48 | -1px 1px 0 green, 49 | 1px 1px 10px green; 50 | } 51 | 52 | .loader { 53 | .title-screen-base(); 54 | background-color: black; 55 | color: #a1a1a1; 56 | padding-top: 25vh; 57 | transition: opacity 1s, visibility 0s 1.5s; 58 | h1 { 59 | text-transform: uppercase; 60 | font-size: 10vw; 61 | } 62 | &.hidden { 63 | visibility: hidden; 64 | opacity: 0; 65 | } 66 | } 67 | 68 | .title-screen-container { 69 | .title-screen-base(); 70 | transition: transform 2s cubic-bezier(.88,.03,.89,.69), opacity 1s 1s, visibility 0s 2s; 71 | &.hidden { 72 | transform: translate3d(0px, 200px, 500px) rotateX(90deg); 73 | opacity: 0; 74 | visibility: hidden; 75 | } 76 | 77 | .gh-star-container { 78 | text-align: right; 79 | margin-top: 20px; 80 | opacity: 0.6; 81 | } 82 | .title-container { 83 | margin: 10vh auto; 84 | width: 80vw; 85 | font-size: 13vw; 86 | color: black; 87 | line-height: 10vw; 88 | .green-text-stroke(); 89 | .line1 { 90 | margin-left: -17vw; 91 | font-size: 8vw; 92 | line-height: 6vw; 93 | } 94 | .line2 { 95 | margin-left: -0vw; 96 | } 97 | .line3 { 98 | margin-left: 0; 99 | } 100 | } 101 | .info-container { 102 | color: white; 103 | width: 30vw; 104 | height: 8vh; 105 | overflow: hidden; 106 | transition: all 0.5s; 107 | position: absolute; 108 | top: 67vh; 109 | 110 | &.instructions { 111 | left: 10vw; 112 | } 113 | &.about { 114 | right: 10vw; 115 | } 116 | 117 | &.display { 118 | z-index: 10; 119 | width: 100vw; 120 | height: 100vh; 121 | background-color: rgba(0, 0, 0, 0.90); 122 | top: 0; 123 | &.instructions { 124 | left: 0; 125 | } 126 | &.about { 127 | right: 0; 128 | } 129 | 130 | .info-body { 131 | opacity: 1; 132 | transition: opacity 0.5s 0.5s; 133 | } 134 | .open-info-container { 135 | font-size: 4vw; 136 | line-height: 18vh; 137 | } 138 | } 139 | &:not(.display):hover { 140 | background-color: rgba(255, 255, 255, 0.25); 141 | } 142 | .info-body { 143 | max-width: 500px; 144 | margin: auto; 145 | opacity: 0; 146 | transition: opacity 0.2s; 147 | font-family: verdana, arial, helvetica, sans serif; 148 | text-transform: none; 149 | font-style: normal; 150 | font-weight: 400; 151 | color: #ddd; 152 | } 153 | .open-info-container { 154 | cursor: pointer; 155 | transition: font-size 0.5s, line-height 0.5s; 156 | font-size: 3vw; 157 | line-height: 8vh; 158 | } 159 | .close-info-container { 160 | .title-font(); 161 | cursor: pointer; 162 | } 163 | 164 | .key { 165 | padding: 3px 8px; 166 | background-color: #808080; 167 | border-radius: 3px; 168 | border: 1px solid #999; 169 | display: inline-block; 170 | font-family: Consolas, "Lucida Console", Monaco, "Courier New", monospace; 171 | } 172 | } 173 | } 174 | 175 | .start-sign, .replay-sign { 176 | position: absolute; 177 | bottom: 1vh; 178 | width: 100%; 179 | font-size: 4vw; 180 | animation: flash 1s infinite; 181 | } 182 | 183 | @keyframes flash { 184 | 0% { opacity: 0 } 185 | 100% { opacity: 0.7 } 186 | } 187 | 188 | .game-over-container { 189 | .title-screen-base(); 190 | font-size: 2em; 191 | .green-text-stroke(); 192 | transition: transform 5s cubic-bezier(.88,.03,.26,.94), opacity 1s 1s, visibility 0s 0s; 193 | .game-won { 194 | &.hidden { 195 | display: none; 196 | } 197 | } 198 | 199 | .game-lost { 200 | &.hidden { 201 | display: none; 202 | } 203 | } 204 | 205 | h1 { 206 | font-size: 6vw; 207 | } 208 | h2 { 209 | font-size: 4vw; 210 | } 211 | 212 | .score-card { 213 | ul { 214 | list-style-type: none; 215 | padding: 0; 216 | } 217 | border: 1px solid rgb(15, 61, 15); 218 | background-color: rgba(0, 0, 0, 0.34); 219 | max-width: 400px; 220 | margin: auto; 221 | } 222 | 223 | .new-record { 224 | display: none; 225 | color: #a0a000; 226 | font-size: 0.5em; 227 | margin-left: 10px; 228 | } 229 | 230 | .record { 231 | color: #aaa; 232 | font-size: 0.7em; 233 | } 234 | 235 | &.hidden { 236 | //transform: translate3d(0px, 200px, 500px) rotateX(90deg); 237 | opacity: 0; 238 | visibility: hidden; 239 | } 240 | } 241 | 242 | @import "game"; 243 | @import "visualizer"; -------------------------------------------------------------------------------- /styles/background.less: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 0% { transform: rotateZ(0); } 3 | 100% { transform: rotateZ(359); } 4 | } 5 | .background { 6 | animation: spin 600s linear infinite; 7 | } 8 | 9 | @keyframes stars-animation-1 { 10 | 0% { 11 | transform: rotateZ(90deg) translateZ(-300px); 12 | } 13 | 100% { 14 | transform: rotateZ(90deg) translateZ(200px); 15 | } 16 | } 17 | @keyframes stars-animation-2 { 18 | 0% { 19 | transform: rotateZ(180deg) translateZ(0px); 20 | } 21 | 100% { 22 | transform: rotateZ(180deg) translateZ(800px); 23 | } 24 | } 25 | .stars { 26 | position: inherit; 27 | width: 1px; 28 | height: 1px; 29 | animation: stars-animation-1 60s linear infinite alternate; 30 | box-shadow: 31 | 3vw 1vh #ffdad0, 32 | 6vw 49vh #ffd6e7, 33 | 15vw -33vh white, 34 | 17vw -5vh white, 35 | 20vw 10vh white, 36 | 22vw 15vh #bbc3ff, 37 | 26vw 23vh white, 38 | 27vw -26vh white, 39 | 33vw 17vh #f1ffad, 40 | 38vw -2vh #ffd2a3, 41 | 41vw -9vh white, 42 | -2vw 2vh white, 43 | -24vw 45vh #dbffeb, 44 | -6vw 20vh white, 45 | -11vw 34vh #fff6c8, 46 | -18vw -40vh white, 47 | -33vw -23vh #b9cbff, 48 | -4vw 20vh white, 49 | -1vw 30vh #b0ffff; 50 | 51 | &:before { 52 | position: absolute; 53 | content: ''; 54 | width: 2px; 55 | height: 2px; 56 | //animation: stars-animation-1 10s linear infinite alternate; 57 | box-shadow: 58 | 3vw 1vh #ffdad0, 59 | 6vw 49vh #ffd6e7, 60 | 15vw -33vh white, 61 | 17vw -5vh white, 62 | 20vw 10vh white, 63 | 22vw 15vh #bbc3ff, 64 | 26vw 23vh white, 65 | 27vw -26vh white, 66 | 33vw 17vh #f1ffad, 67 | 38vw -2vh #ffd2a3, 68 | 41vw -9vh white, 69 | -2vw 2vh white, 70 | -24vw 45vh #dbffeb, 71 | -6vw 20vh white, 72 | -11vw 34vh #fff6c8, 73 | -18vw -40vh white, 74 | -33vw -23vh #b9cbff, 75 | -4vw 20vh white, 76 | -1vw 30vh #b0ffff; 77 | transform: rotateZ(90deg); 78 | } 79 | &:after { 80 | position: absolute; 81 | content: ''; 82 | width: 1px; 83 | height: 1px; 84 | animation: stars-animation-2 60s linear infinite alternate; 85 | box-shadow: 86 | 3vw 1vh #ffdad0, 87 | 6vw 49vh #ffd6e7, 88 | 15vw -33vh white, 89 | 17vw -5vh white, 90 | 20vw 10vh white, 91 | 22vw 15vh #bbc3ff, 92 | 26vw 23vh white, 93 | 27vw -26vh white, 94 | 33vw 17vh #f1ffad, 95 | 38vw -2vh #ffd2a3, 96 | 41vw -9vh white, 97 | -2vw 2vh white, 98 | -24vw 45vh #dbffeb, 99 | -6vw 20vh white, 100 | -11vw 34vh #fff6c8, 101 | -18vw -40vh white, 102 | -33vw -23vh #b9cbff, 103 | -4vw 20vh white, 104 | -1vw 30vh #b0ffff; 105 | transform: rotateZ(180deg); 106 | } 107 | } 108 | 109 | 110 | @keyframes planet-animation { 111 | 0% { 112 | transform: translateZ(0px); 113 | } 114 | 100% { 115 | transform: translateZ(300px); 116 | } 117 | } 118 | .planet { 119 | position: absolute; 120 | z-index: 1; 121 | top: -25vw; 122 | left: -75vw; 123 | width: 50vw; 124 | height: 50vw; 125 | border-radius: 50%; 126 | background-color: #111; 127 | animation: planet-animation 60s linear infinite alternate; 128 | box-shadow: 129 | inset 0 0 50px #fff, 130 | inset 20px 0 80px #f0f, 131 | inset -20px 0 80px #0ff, 132 | inset 20px 0 300px #f0f, 133 | inset -20px 0 300px #0ff, 134 | 0 0 50px #fff, 135 | -10px 0 80px #f0f, 136 | 10px 0 80px #0ff; 137 | } 138 | 139 | .background { 140 | 141 | } -------------------------------------------------------------------------------- /styles/game.less: -------------------------------------------------------------------------------- 1 | @import "vars"; 2 | 3 | #player { 4 | display: none; 5 | } 6 | 7 | .scene { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | bottom: 0; 13 | perspective: 800px; 14 | transform-origin: center center; 15 | overflow: hidden; 16 | } 17 | 18 | .face { 19 | position: absolute; 20 | top: 50%; 21 | left: 50%; 22 | transform-style: preserve-3d; 23 | } 24 | 25 | 26 | .overlay { 27 | position: absolute; 28 | overflow: hidden; 29 | perspective: 800px; 30 | top: 0; 31 | left: 0; 32 | bottom: 0; 33 | right: 0; 34 | } 35 | 36 | .firepower-meter-container { 37 | position: absolute; 38 | opacity: 0.5; 39 | left: 20px; 40 | bottom: 20px; 41 | z-index: 300; 42 | overflow: hidden; 43 | width: 300px; 44 | height: 30px; 45 | transition: transform 0.5s 4s; 46 | &.hidden { 47 | transform: translateX(-500px); 48 | } 49 | .firepower-meter { 50 | display: block; 51 | position: relative; 52 | border-radius: 5px; 53 | width: 300px; 54 | height: 30px; 55 | //background: linear-gradient(to right, rgba(170,34,34,1) 0%,rgba(232,131,37,1) 21%,rgba(0,226,7,1) 60%, rgb(255, 255, 255) 87%); /* W3C */ 56 | background: linear-gradient(to right, rgb(0, 19, 100) 0%, rgb(0, 11, 150) 21%, rgb(0, 152, 255) 60%, rgb(255, 255, 255) 87%); /* W3C */ 57 | span { 58 | position: absolute; 59 | .title-font(); 60 | text-transform: uppercase; 61 | color: rgba(255, 255, 255, 0.62); 62 | font-size: 27px; 63 | top: -2px; 64 | left: 10px; 65 | } 66 | } 67 | } 68 | 69 | .announcement { 70 | opacity: 0; 71 | visibility: hidden; 72 | position: absolute; 73 | width: 100%; 74 | margin-top: 40vh; 75 | .title-font(); 76 | color: white; 77 | text-align: center; 78 | text-transform: uppercase; 79 | transition: opacity 0.5s, visibility 0s; 80 | &.visible { 81 | opacity: 0.8; 82 | visibility: visible; 83 | } 84 | .title { 85 | font-size: 12vh; 86 | } 87 | .subtitle { 88 | font-size: 8vh; 89 | } 90 | } 91 | 92 | .score-container { 93 | position: absolute; 94 | bottom: 10px; 95 | right: 10px; 96 | transition: transform 0.5s 4s; 97 | &.hidden { 98 | transform: translateX(500px); 99 | } 100 | .score { 101 | color: white; 102 | font-size: 8vh; 103 | font-family: @title-font; 104 | font-weight: 900; 105 | } 106 | } 107 | 108 | .lives-container { 109 | position: absolute; 110 | bottom: 80px; 111 | right: 15px; 112 | transition: transform 0.5s 4.2s; 113 | &.hidden { 114 | transform: translateX(500px); 115 | } 116 | .life { 117 | font-size: 45px; 118 | color: white; 119 | display: inline-block; 120 | opacity: 0.7; 121 | transition: opacity 1s; 122 | &.hidden { 123 | opacity: 0; 124 | } 125 | } 126 | } 127 | 128 | @import "background"; 129 | @import "ship"; 130 | @import "track"; 131 | @import "shot"; 132 | @import "alien"; -------------------------------------------------------------------------------- /styles/ship.less: -------------------------------------------------------------------------------- 1 | @import "vars"; 2 | /** 3 | * The ship 4 | */ 5 | @ship-color: blue; 6 | @ship-box-shadow: inset 0 0 10px @ship-color, 0px 0px 10px 0px @ship-color; 7 | @ship-fill-color: rgba(0,0,0,0.5); 8 | @ship-border-width: @scale * 0.15; 9 | 10 | .ship-container { 11 | position: inherit; 12 | z-index: 50; 13 | } 14 | .ship { 15 | border: @ship-border-width solid @ship-color; 16 | box-shadow: @ship-box-shadow; 17 | } 18 | 19 | @hull-width: 2.5 * @scale; 20 | @hull-height: 10 * @scale; 21 | @ship-wedge-angle: 5deg; 22 | .hull { 23 | width: @hull-width; 24 | height: @hull-height; 25 | margin: -@hull-height/2 -@hull-width/2; 26 | background-color: @ship-fill-color; 27 | 28 | &.top { 29 | transform-origin: top center; 30 | transform: rotateX(@ship-wedge-angle); 31 | } 32 | &.bottom { 33 | transform-origin: top center; 34 | transform: rotateX(-@ship-wedge-angle); 35 | } 36 | &.back { 37 | @hull-back-height: @hull-height * sin(@ship-wedge-angle*2); 38 | @margin-top: ((@hull-height * cos(@ship-wedge-angle))/2) - @hull-back-height/2; 39 | margin: @margin-top -@hull-width/2; 40 | height: @hull-back-height; 41 | transform-origin: center center; 42 | transform: rotateX(90deg); 43 | } 44 | } 45 | 46 | 47 | /* 48 | // does not work for some reason 49 | .generate-booster-animation(@name, @color) { 50 | .keyframe(@n, @i: 0) when (@i =< @n) { 51 | @percentage: percentage(@i/10); 52 | @{percentage} { 53 | @random: `Math.random()`; 54 | @fade: percentage(@random); 55 | background-color: fade(@color, @fade); 56 | width: @fade; 57 | } 58 | .keyframe(@n, (@i + 1)); 59 | } 60 | 61 | @keyframes @name { 62 | .keyframe(10); 63 | } 64 | } 65 | .generate-booster-animation(booster-amination-1, white); 66 | .generate-booster-animation(booster-amination-2, red); 67 | .generate-booster-animation(booster-amination-3, orange);*/ 68 | 69 | .booster { 70 | @booster-diameter: @hull-width * 0.6; 71 | @margin-top: ((@hull-height * cos(@ship-wedge-angle))/2) - @booster-diameter/2; 72 | width: @booster-diameter; 73 | height: @booster-diameter; 74 | margin: @margin-top -@booster-diameter/2; 75 | border-radius: 50% 50%; 76 | transform: rotateX(90deg); 77 | animation: booster-animation-1 10s infinite; 78 | &:before { 79 | content: ''; 80 | position: absolute; 81 | display: inline-block; 82 | width: @booster-diameter * 0.9; 83 | height: @booster-diameter * 0.9; 84 | margin: (@booster-diameter * 0.05)-@ship-border-width; 85 | border-radius: 50% 50%; 86 | transform: translateZ(@scale*0.4); 87 | } 88 | &:after { 89 | content: ''; 90 | position: absolute; 91 | display: inline-block; 92 | width: @booster-diameter * 1.1; 93 | height: @booster-diameter * 1.1; 94 | margin: -(@booster-diameter * 0.05)-@ship-border-width; 95 | border-radius: 50% 50%; 96 | border: @ship-border-width solid lighten(@ship-color, 30%); 97 | transform: translateZ(-@scale*0.4); 98 | } 99 | } 100 | 101 | @wing-width: 20 * @scale; 102 | @wing-height: 5 * @scale; 103 | .wing { 104 | width: @wing-width; 105 | height: @wing-height; 106 | margin: 0 -@wing-width/2; 107 | border-radius: 50% 50% 0 0; 108 | border-width: @ship-border-width*2; 109 | border-bottom-width: @ship-border-width; 110 | background-color: @ship-fill-color; 111 | 112 | &.top { 113 | transform-origin: top center; 114 | transform: rotateX(@ship-wedge-angle); 115 | } 116 | &.bottom { 117 | transform-origin: top center; 118 | transform: rotateX(-@ship-wedge-angle); 119 | } 120 | &.back { 121 | @wing-back-height: @wing-height * sin(@ship-wedge-angle*2); 122 | @margin-top: (@wing-height * cos(@ship-wedge-angle)) - @wing-back-height/3; 123 | border-width: @ship-border-width; 124 | margin: @margin-top -@wing-width/2; 125 | height: @wing-back-height; 126 | transform-origin: center center; 127 | transform: rotateX(90deg); 128 | border-radius: 0; 129 | } 130 | } 131 | .gun { 132 | width: 0.1 * @scale; 133 | height: 2 * @scale; 134 | &.right { 135 | margin: -@wing-height/5 @wing-width/4; 136 | } 137 | &.left { 138 | margin: -@wing-height/5 -@wing-width/4; 139 | } 140 | } -------------------------------------------------------------------------------- /styles/shot.less: -------------------------------------------------------------------------------- 1 | @import "vars"; 2 | 3 | .shot { 4 | display: none; 5 | position: absolute; 6 | z-index: 25; 7 | width: @scale * 1; 8 | height: @scale * 10; 9 | margin-top: -@scale * 5; 10 | border-radius: 50%; 11 | background-color: rgb(255, 255, 255, 0.8); 12 | border: @scale solid rgba(11, 0, 255, 0.5); 13 | /*box-shadow: 14 | 0 0 30px 15px #fff, *//* inner white *//* 15 | 0 0 50px 30px rgba(255, 0, 255, 0.62), *//* middle magenta *//* 16 | 0 0 70px 45px rgba(0, 255, 255, 0.53); *//* outer cyan */ 17 | &:after { 18 | position: absolute; 19 | content: ''; 20 | width: @scale * 2; 21 | height: @scale * 2; 22 | background-color: rgba(156, 148, 255, 0.50); 23 | border-radius: 50%; 24 | margin-left: -@scale + @scale/2; 25 | transform: rotateX(90deg); 26 | } 27 | } -------------------------------------------------------------------------------- /styles/track.less: -------------------------------------------------------------------------------- 1 | @import "vars"; 2 | /** 3 | * The track 4 | */ 5 | @track-color: green; 6 | @track-line-width: 8px; 7 | @track-box-shadow: inset 0 0 30px @track-color, 0px 0px 30px 0px @track-color; 8 | @track-tile-width: 60 * @scale; 9 | @track-tile-height: 600px; 10 | .track-container { 11 | position: inherit; 12 | z-index: 10; 13 | transform-style: preserve-3d; 14 | } 15 | .track { 16 | opacity: 0; 17 | width: @track-tile-width; 18 | height: @track-tile-height; 19 | margin: -@track-tile-height/2 -@track-tile-width/2; 20 | border: @track-line-width solid @track-color; 21 | box-shadow: @track-box-shadow; 22 | border-left-width: @track-line-width/3; 23 | border-right-width: @track-line-width/3; 24 | } 25 | .track:before { 26 | content: ''; 27 | position: absolute; 28 | width: @track-tile-width/3; 29 | height: @track-tile-height; 30 | border-left: @track-line-width/3 solid @track-color; 31 | border-right: @track-line-width/3 solid @track-color; 32 | box-shadow: @track-box-shadow; 33 | left: @track-tile-width/3;; 34 | } 35 | .track:after { 36 | content: ''; 37 | position: absolute; 38 | width: @track-tile-width; 39 | height: @track-tile-height/3; 40 | border-top: @track-line-width solid @track-color; 41 | border-bottom: @track-line-width solid @track-color; 42 | box-shadow: @track-box-shadow; 43 | top: @track-tile-height/3; 44 | } 45 | .track.top { 46 | } 47 | .track.bottom { 48 | } 49 | .generate-track-animation-definition(@name, @translateY) { 50 | @keyframes @name { 51 | 0% { 52 | opacity: 0; 53 | transform: translateY(@translateY) translateZ(-2000px) rotateX(90deg); 54 | } 55 | 30% { 56 | opacity: 1; 57 | } 58 | 100% { 59 | opacity: 1; 60 | transform: translateY(@translateY) translateZ(1000px) rotateX(90deg); 61 | } 62 | } 63 | } 64 | .generate-track-animation-definition(track-animation-top, -@scale * 15); 65 | .generate-track-animation-definition(track-animation-bottom, @scale * 15); 66 | 67 | .generate-track-ids(@n, @i: 1) when (@i =< @n) { 68 | .track.top:nth-child(@{i}) { 69 | animation: track-animation-top 3s linear (@i - 1) * 0.6s infinite normal; 70 | } 71 | .track.bottom:nth-child(@{i}) { 72 | animation: track-animation-bottom 3s linear (@i - 1) * 0.6s infinite normal; 73 | } 74 | .generate-track-ids(@n, (@i + 1)); 75 | } 76 | .generate-track-ids(6); 77 | -------------------------------------------------------------------------------- /styles/vars.less: -------------------------------------------------------------------------------- 1 | @scale: 1vw; 2 | .title-font() { 3 | font-family: 'Exo', sans-serif; 4 | font-weight: 900; 5 | font-style: italic; 6 | } 7 | @title-font: 'Exo', sans-serif; -------------------------------------------------------------------------------- /styles/visualizer.less: -------------------------------------------------------------------------------- 1 | .visualizer { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | 8 | .highs { 9 | width: 100%; 10 | background: linear-gradient(to top, rgba(0,0,0,0) 0%,rgba(89,86,255,0.8) 100%); 11 | height: 25vh; 12 | transition: opacity 0.1s linear; 13 | } 14 | 15 | .lows { 16 | position: absolute; 17 | width: 100%; 18 | bottom: 0; 19 | background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(89,86,255,0.8) 100%); 20 | height: 25vh; 21 | transition: opacity 0.1s linear; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vendor/prefixfree.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * StyleFix 1.0.3 & PrefixFree 1.0.7 3 | * @author Lea Verou 4 | * MIT license 5 | */(function(){function t(e,t){return[].slice.call((t||document).querySelectorAll(e))}if(!window.addEventListener)return;var e=window.StyleFix={link:function(t){try{if(t.rel!=="stylesheet"||t.hasAttribute("data-noprefix"))return}catch(n){return}var r=t.href||t.getAttribute("data-href"),i=r.replace(/[^\/]+$/,""),s=(/^[a-z]{3,10}:/.exec(i)||[""])[0],o=(/^[a-z]{3,10}:\/\/[^\/]+/.exec(i)||[""])[0],u=/^([^?]*)\??/.exec(r)[1],a=t.parentNode,f=new XMLHttpRequest,l;f.onreadystatechange=function(){f.readyState===4&&l()};l=function(){var n=f.responseText;if(n&&t.parentNode&&(!f.status||f.status<400||f.status>600)){n=e.fix(n,!0,t);if(i){n=n.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi,function(e,t,n){return/^([a-z]{3,10}:|#)/i.test(n)?e:/^\/\//.test(n)?'url("'+s+n+'")':/^\//.test(n)?'url("'+o+n+'")':/^\?/.test(n)?'url("'+u+n+'")':'url("'+i+n+'")'});var r=i.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1");n=n.replace(RegExp("\\b(behavior:\\s*?url\\('?\"?)"+r,"gi"),"$1")}var l=document.createElement("style");l.textContent=n;l.media=t.media;l.disabled=t.disabled;l.setAttribute("data-href",t.getAttribute("href"));a.insertBefore(l,t);a.removeChild(t);l.media=t.media}};try{f.open("GET",r);f.send(null)}catch(n){if(typeof XDomainRequest!="undefined"){f=new XDomainRequest;f.onerror=f.onprogress=function(){};f.onload=l;f.open("GET",r);f.send(null)}}t.setAttribute("data-inprogress","")},styleElement:function(t){if(t.hasAttribute("data-noprefix"))return;var n=t.disabled;t.textContent=e.fix(t.textContent,!0,t);t.disabled=n},styleAttribute:function(t){var n=t.getAttribute("style");n=e.fix(n,!1,t);t.setAttribute("style",n)},process:function(){t('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link);t("style").forEach(StyleFix.styleElement);t("[style]").forEach(StyleFix.styleAttribute)},register:function(t,n){(e.fixers=e.fixers||[]).splice(n===undefined?e.fixers.length:n,0,t)},fix:function(t,n,r){for(var i=0;i-1&&(e=e.replace(/(\s|:|,)(repeating-)?linear-gradient\(\s*(-?\d*\.?\d*)deg/ig,function(e,t,n,r){return t+(n||"")+"linear-gradient("+(90-r)+"deg"}));e=t("functions","(\\s|:|,)","\\s*\\(","$1"+s+"$2(",e);e=t("keywords","(\\s|:)","(\\s|;|\\}|$)","$1"+s+"$2$3",e);e=t("properties","(^|\\{|\\s|;)","\\s*:","$1"+s+"$2:",e);if(n.properties.length){var o=RegExp("\\b("+n.properties.join("|")+")(?!:)","gi");e=t("valueProperties","\\b",":(.+?);",function(e){return e.replace(o,s+"$1")},e)}if(r){e=t("selectors","","\\b",n.prefixSelector,e);e=t("atrules","@","\\b","@"+s+"$1",e)}e=e.replace(RegExp("-"+s,"g"),"-");e=e.replace(/-\*-(?=[a-z]+)/gi,n.prefix);return e},property:function(e){return(n.properties.indexOf(e)>=0?n.prefix:"")+e},value:function(e,r){e=t("functions","(^|\\s|,)","\\s*\\(","$1"+n.prefix+"$2(",e);e=t("keywords","(^|\\s)","(\\s|$)","$1"+n.prefix+"$2$3",e);n.valueProperties.indexOf(r)>=0&&(e=t("properties","(^|\\s|,)","($|\\s|,)","$1"+n.prefix+"$2$3",e));return e},prefixSelector:function(e){return e.replace(/^:{1,2}/,function(e){return e+n.prefix})},prefixProperty:function(e,t){var r=n.prefix+e;return t?StyleFix.camelCase(r):r}};(function(){var e={},t=[],r={},i=getComputedStyle(document.documentElement,null),s=document.createElement("div").style,o=function(n){if(n.charAt(0)==="-"){t.push(n);var r=n.split("-"),i=r[1];e[i]=++e[i]||1;while(r.length>3){r.pop();var s=r.join("-");u(s)&&t.indexOf(s)===-1&&t.push(s)}}},u=function(e){return StyleFix.camelCase(e)in s};if(i.length>0)for(var a=0;a