├── .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 | 
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 |
29 |
30 |
31 |
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 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | Firepower
86 |
87 |
91 |
94 |
95 |
☆
96 |
☆
97 |
☆
98 |
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 |
142 |
143 |
Congratulations!
144 | You Won!
145 |
146 |
147 |
148 |
149 |
You Lost!
150 | Better Luck Next Time
151 |
152 |
153 |
154 |
155 |
156 | Stage: New Record!
157 | Record:
158 |
159 |
160 | Score: New Record!
161 | Record:
162 |
163 |
164 |
165 |
166 |
Press "R" to replay
167 |
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 |
28 |
29 |
30 |
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 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | Firepower
85 |
86 |
90 |
93 |
94 |
☆
95 |
☆
96 |
☆
97 |
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 |
141 |
142 |
Congratulations!
143 | You Won!
144 |
145 |
146 |
147 |
148 |
You Lost!
149 | Better Luck Next Time
150 |
151 |
152 |
153 |
154 |
155 | Stage: New Record!
156 | Record:
157 |
158 |
159 | Score: New Record!
160 | Record:
161 |
162 |
163 |
164 |
165 |
Press "R" to replay
166 |
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