<!DOCTYPE html>
<html>
<head>
<meta charset=“UTF-8”/>
<title>3D Music sequencer</title>
</head>
<style>
body {
background-color: #111;
font-family: sans-serif;
color: #aaa;
font-size: 1em;
text-align: center;
}
#container {
width: 100%;
margin: 0 auto;
margin-top: 1em;
display: flex;
justify-content: center;
}
canvas {
background-color: #000;
border-radius: 10px;
box-shadow: 0 0 20px white;
}
#instructions {
margin: 0 auto;
width: 200px;
text-align: left;
display: block;
margin-top: 10px;
}
</style>
</head>
<body>
<div id=“container”>
<canvas id=“GameCanvas” width=“800” height=“600”></canvas>
</div>
<div id=“instructions”>
Instructions:<br>
<ul>
<li><b>W/S/A/D</b>: Movement</li>
<li><b>Mouse</b>: Look around</li>
<li><b>Space</b>: Play/Pause</li>
<li><b>Q/E</b>: Change Instrument</li>
</ul>
</div>
<script>
var canvas = document.getElementById(‘GameCanvas’);
var context = canvas.getContext(‘2d’);

var lastTime;
function main() {
var now = Date.now();
var dt = (now - lastTime) / 1000.0;

update(dt);
render();

lastTime = now;
requestAnimFrame(main);
};

resources.load([‘assets/sprites.png’,‘assets/sprites.json’,‘assets/instruments.json’]);
resources.onReady(init);

var player;
var keysDown = {};
var sounds = [];
var bgMusic;
var instrumentIndex = 0;
var instruments;

function init() {
reset();
lastTime = Date.now();
main();

document.addEventListener(‘keydown’, function(e) {
keysDown[e.keyCode] = true;
});

document.addEventListener(‘keyup’, function(e) {
delete keysDown[e.keyCode];
});

//Load instruments
instruments = JSON.parse(resources.get(‘assets/instruments.json’));
bgMusic = new Audio(‘assets/bg.mp3’);
bgMusic.volume = 0.4;
bgMusic.play();
for (var i = 0; i < instruments.length; i++)
{
sounds[i] = new Audio(‘assets/’ + instruments[i] + ‘.mp3’);
sounds[i].volume = 0.4;
}
}

function update(dt) {
updateEntities(dt);
checkCollisions();
}

function updateEntities(dt) {
player.update(dt);
}

function checkCollisions() {
player.checkCollisions();
}

function render() {
context.fillStyle = “#000”;
context.fillRect(0, 0, canvas.width, canvas.height);

renderEntity(player);
for (var i = 0; i < player.bullets.length; i++) {
renderEntity(player.bullets[i]);
}
}

function renderEntity(entity) {
context.save();
context.translate(entity.x, entity.y);
context.scale(entity.scale, entity.scale);
entity.sprite.render(context);
context.restore();
}

function reset() {
player = new Player();
}

// The browser API key obtained from the Google API Console.
// Replace with your own browser API key, or your own key.
var developerKey = ‘AIzaSyCZJlH2vD3WlK_oVJ4yh1sHv8tWLhTKbS4’;

// The Client ID obtained from the Google API Console. Replace with your own Client ID.
var clientId = “1013620148359-vb9m9d0jii2i2j6vjpgv58l1ebhc5b6a.apps.googleusercontent.com”

// Scope to use to access user’s photos.
var scope = [‘https://www.googleapis.com/auth/drive.readonly’];

var pickerApiLoaded = false;
var oauthToken;

// Use the API Loader script to load google.picker and gapi.auth.
function onApiLoad() {
gapi.load(‘auth’, {‘callback’: onAuthApiLoad});
gapi.load(‘picker’, {‘callback’: onPickerApiLoad});
}

function onAuthApiLoad() {
window.gapi.auth.authorize(
{
‘client_id’: clientId,
‘scope’: scope,
‘immediate’: false
},
handleAuthResult);
}

function onPickerApiLoad() {
pickerApiLoaded = true;
createPicker();
}

function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
oauthToken = authResult.access_token;
createPicker();
}
}

// Create and render a Picker object for searching images.
function createPicker() {
if (pickerApiLoaded && oauthToken) {
var view = new google.picker.View(google.picker.ViewId.DOCS);
view.setMimeTypes(“audio/mpeg”);
var picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setAppId(clientId)
.setOAuthToken(oauthToken)
.addView(view)
.addView(new google.picker.DocsUploadView())
.setDeveloperKey(developerKey)
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
}

// A simple callback implementation.
function pickerCallback(data) {
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
var fileId = data[google.picker.Response.DOCUMENTS][0][‘id’];
gapi.client.drive.files.get({
‘fileId’: fileId
}).then(function(resp) {
bgMusic.pause();
bgMusic = new Audio(resp.result.webContentLink);
bgMusic.volume = 0.4;
bgMusic.play();
console.log('You have selected: ’ + bgMusic.src);
});
}
}

//Pause/Play
document.onkeydown = function(evt) {
evt = evt || window.event;
//Space
if (evt.keyCode == 32) {
if (bgMusic.paused)
bgMusic.play();
else
bgMusic.pause();
}
//Q
if (evt.keyCode == 81) {
if (instrumentIndex > 0)
instrumentIndex–;
else
instrumentIndex = 3;
console.log('Instrument: ’ + instruments[instrumentIndex]);
}
//E
if (evt.keyCode == 69) {
if (instrumentIndex < 3)
instrumentIndex++;
else
instrumentIndex = 0;
console.log('Instrument: ’ + instruments[instrumentIndex]);
}
};

//Sprite
function Sprite(url, pos, size, speed, frames, dir, once) {
this.pos = pos;
this.size = size;
this.speed = typeof speed === ‘number’ ? speed : 0;
this.frames = frames;
this._index = 0;
this.url = url;
this.dir = dir || ‘horizontal’;
this.once = once;
};

Sprite.prototype = {
update: function(dt) {
this._index += this.speed*dt;
},

render: function(ctx) {
var frame;

if(this.speed > 0) {
var max = this.frames.length;
var idx = Math.floor(this._index);
frame = this.frames[idx % max];

if(this.once && idx >= max) {
this.done = true;
return;
}
}
else {
frame = 0;
}


var x = this.pos[0];
var y = this.pos[1];

if(this.dir == ‘vertical’) {
y += frame * this.size[1];
}
else {
x += frame * this.size[0];
}

ctx.drawImage(resources.get(this.url),
x, y,
this.size[0], this.size[1],
0, 0,
this.size[0], this.size[1]);
}
};

//Bullet
function Bullet(pos, dir) {
this.pos = [pos[0], pos[1]];
this.dir = dir;
this.sprite = new Sprite(‘assets/sprites.png’,[0,0],[32,32],6,[0,1], ‘horizontal’, true);
this.speed = 500;
this.done = false;
}

Bullet.prototype.update = function(dt) {
this.pos[0] += this.dir[0] * this.speed * dt;
this.pos[1] += this.dir[1] * this.speed * dt;
this.sprite.update(dt);
};

Bullet.prototype.render = function(ctx) {
ctx.save();

ctx.translate(this.pos[0], this.pos[1]);
if (this.dir[0] < 0)
ctx.scale(-1,1);
this.sprite.render(ctx);

ctx.restore();
};

//Player
function Player() {
this.pos = [400, 300];
this.size = [32, 32];
this.speed = 200;
this.bullets = [];
this.cooldown = 0;
this.scale = 1;
this.sprite = new Sprite(‘assets/sprites.png’, [0, 0], this.size, 0, [0], ‘horizontal’, false);
this.dir = [0, 0];
this.frame = 0;
this.angle = 0;
this.speed = 0;
this.maxspeed = 200;
this.accel = 500;
this.friction = 12;
this.padding = [20, 20];
this.bounds = [
[this.padding[0], canvas.width - this.padding[0]],
[this.padding[1], canvas.height - this.padding[1]]
];
this.invuln = 0;
}

Player.prototype.update = function(dt) {
this.invuln -= dt;
this.frame += dt * 5;
if (this.frame >= 4)
this.frame = 0;
this.sprite.pos = [this.frame * this.size[0], 0];
if (this.invuln > 0)
this.sprite.pos = [this.frame * this.size[0], this.size[1]];

this.speed *= Math.pow(1 - this.friction, dt);

if (37 in keysDown) { //left
this.dir[0] = -1;
}
if (39 in keysDown) { //right
this.dir[0] = 1;
}
if (38 in keysDown) { //up
this.dir[1] = -1;
}
if (40 in keysDown) { //down
this.dir[1] = 1;
}
if (32 in keysDown) { //space
this.cooldown -= dt;
if (this.cooldown <= 0) {
this.cooldown = 0.3;
this.shoot();
}
}
if (this.dir[0] || this.dir[1]) {
this.speed += this.accel * dt;
if (this.speed > this.maxspeed)
this.speed = this.maxspeed;
}
var x = this.pos[0] + this.dir[0] * this.speed * dt;
var y = this.pos[1] + this.dir[1] * this.speed * dt;

this.pos[0] = clamp(x, this.bounds[0][0], this.bounds[0][1]);
this.pos[1] = clamp(y, this.bounds[1][0], this.bounds[1][1]);

var dx = canvas.width/2 - this.pos[0];
var dy = canvas.height/2 - this.pos[1];
this.angle = Math.atan2(dy, dx) + Math.PI;

this.sprite.update(dt);
};

Player.prototype.render = function() {
context.save();

context.translate(this.pos[0], this.pos[1]);
context.rotate(this.angle);
context.scale(this.scale, this.scale);
this.sprite.render(context);

context.restore();
};

Player.prototype.shoot = function() {
var dir = [Math.cos(this.angle), Math.sin(this.angle)];
var pos = [this.pos[0] + this.size[0]/2, this.pos[1] + this.size[1]/2];
var bullet = new Bullet(pos, dir);
this.bullets.push(bullet);
sounds[instrumentIndex].currentTime = 0;
sounds[instrumentIndex].play();
};

Player.prototype.checkCollisions = function() {
if (this.invuln <= 0) {
for (var i=0; i<enemies.length; i++) {
var enemy = enemies[i];
if (collides(this, enemy)) {
this.invuln = 2;
reset();
bgMusic.currentTime = 0;
bgMusic.play();
}
}
}
};

//Enemy
function Enemy() {
this.pos = [rand(canvas.width), rand(canvas.height)];
this.size = [32, 32];
this.speed = [rand(300, 500), rand(300, 500)];
this.direction = [randInt(-1, 1), randInt(-1, 1)];
this.sprite = new Sprite(‘assets/sprites.png’, [32, 0], this.size, 6, [0, 1], ‘horizontal’, true);
}

Enemy.prototype.update = function(dt) {
this.sprite.update(dt);

this.pos[0] += this.speed[0] * dt * this.direction[0];
this.pos[1] += this.speed[1] * dt * this.direction[1];

if (this.pos[0] < this.size[0] || this.pos[0] > canvas.width - this.size[0]) {
this.direction[0] *= -1;
}

if (this.pos[1] < this.size[1] || this.pos[1] > canvas.height - this.size[1]) {
this.direction[1] *= -1;
}
};

Enemy.prototype.render = function() {
context.save();

context.translate(this.pos[0], this.pos[1]);
this.sprite.render(context);

context.restore();
};

//Utility functions
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1));
}

function randInt(min, max) {
return min + Math.floor(Math.random() * (max - min + 1));
}

function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}

function collides(a, b) {
return a.pos[0] < b.pos[0] + b.size[0] &&
a.pos[0] + a.size[0] > b.pos[0] &&
a.pos[1] < b.pos[1] + b.size[1] &&
a.pos[1] + a.size[1] > b.pos[1];
}

// requestAnimationFrame polyfill
var requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback){
window.setTimeout(callback, 1000 / 60);
};
})();

//Resources
var resources = {
images: {},
data: {}
};

resources.load = function (urls) {
var self = this;
var load = function (url) {
var extension = url.split(‘.’).pop().toLowerCase();

if (extension === ‘png’ || extension === ‘jpg’ || extension === ‘jpeg’ || extension === ‘gif’) {
var img = new Image();
img.src = url;
img.onload = function () { self.images[url] = img; };
}
else if (extension === ‘json’) {
var xhr = new XMLHttpRequest();
xhr.open(‘GET’, url, true);
xhr.onload = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var obj = JSON.parse(xhr.responseText);
self.data[url] = obj;
}
}
};
xhr.send(null);
}
else if (extension === ‘mp3’){
self.data[url] = url;
}
};
for (var i = 0; i < urls.length; ++i) {
load(urls[i]);
}
};

resources.get = function (url) {
if (url in this.images) {
return this.images[url];
}
else {
return this.data[url];
}
};

resources.onReady = function (func) {
var self = this;
var check = function () {
for (var url in self.images) {
if (!self.images[url].complete) {
setTimeout(check, 100);
return;
}
}
func();
};
check();
};
</script>
<script>
var chiptune