var canvas = document.getElementById('game'); var ctx = canvas.getContext('2d'); var beatUrls = [ 'sound/b1.mp3', 'sound/b2.mp3', 'sound/b3.mp3', 'sound/b4.mp3', 'sound/b5.mp3' ]; var createSound = function(name, url) { var deferred = Q.defer(); soundManager.createSound({ id: name, url: url, autoLoad: true, onload: function() { deferred.resolve(this); } }); return deferred.promise; }; var starColors = [ 'maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'teal', 'blue', 'navy', 'purple' ]; var paused = false; var debug = false; var beats = []; var beatTime = 0; var currentBeats = []; var gotPoint = false; var switchBeat = function(index) { var currentBeat = beats[index]; activeTarget.beat = currentBeat; currentBeats.push(currentBeat); if(currentBeats.length > 4) { currentBeats.splice(0, 1); } }; // Sound var soundDeferred = Q.defer(); var soundPromise = soundDeferred.promise; soundManager.setup({ url: 'swf/', flashVersion: 9, onready: function() { soundDeferred.resolve(this); } }); soundPromise = soundPromise.then(function() { var beatPromises = []; beatUrls.map(function(elem, index) { beatPromises.push(createSound('beat'+index, elem)); }); return Q.spread([ Q.all(beatPromises), createSound('get', 'sound/get.mp3') ], function(beatz) { beats = beatz; }) }); // Input handling var keysDown = {}; addEventListener('keydown', function(e) { keysDown[e.keyCode] = true; }, false); addEventListener('keyup', function(e) { if(e.keyCode == 68) { debug = !debug; } if(e.keyCode == 80) { pause(); } delete keysDown[e.keyCode]; }, false); addEventListener('blur', function() { if(!paused) pause(); }); // Constants var RADIUS = 8; var LEAF_SIZE = 8; var LEVEL_SIZE = 800; var ALLOWANCE = (LEVEL_SIZE - canvas.width)/2; var METER_WIDTH = 120; var METER_HEIGHT = 40; var METER_SPEED = 10; var PLAYER_SPEED = 50; var STAGE_SPEED = 25; var RIPPLE_SIZE = 10; var RIPPLE_SPEED = 10; var DOT_SIZE = 3; var DOT_DISTANCE = 4 * 5; // Drawing functions function drawCircle(point, r, w, color) { ctx.beginPath(); ctx.arc(point.x, point.y, r, 0, 2*Math.PI, false); ctx.lineWidth = w; ctx.strokeStyle = color; ctx.stroke(); } function drawLeafV(point, h, w) { ctx.beginPath(); ctx.moveTo(point.x, point.y); ctx.quadraticCurveTo(point.x + w, point.y + h/2, point.x, point.y + h); ctx.quadraticCurveTo(point.x - w, point.y + h/2, point.x, point.y); ctx.fill(); } function drawLeafH(point, w, h) { ctx.beginPath(); ctx.moveTo(point.x, point.y); ctx.quadraticCurveTo(point.x + w/2, point.y + h, point.x + w, point.y); ctx.quadraticCurveTo(point.x + w/2, point.y - h, point.x, point.y); ctx.fill(); } function drawPlayer(point) { drawCircle(point, RADIUS, 5, "#FFFFFF"); } function drawSpectrum() { var barHeight = 30; var barWidth = 6; var barSpacing = 2; var margin = 5; ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.beginPath(); ctx.rect( margin - ctx.lineWidth, canvas.height - barHeight - ctx.lineWidth - margin, activeTarget.spectrum.length * (barWidth+barSpacing) + 2 * ctx.lineWidth - barSpacing, barHeight + 2 * ctx.lineWidth ); ctx.stroke(); for(var i = 0; i < activeTarget.spectrum.length; ++i) { var height = activeTarget.spectrum[i]*20; ctx.fillStyle = starColors[i]; ctx.fillRect(i * (barWidth + barSpacing) + margin, canvas.height - height - margin, barWidth, height); } } function sign(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; } function lerp(from, to, p) { return to * p + from * (1 - p); } function ease(v) { return v * v * (3 - 2 * v); } function easeOutExpo(t, b, c, d) { return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; } function color(r,g,b,a) { return 'rgba('+r+','+g+','+b+','+a.toFixed(5)+')'; } function distX(a, b) { var dx = a.x - b.x; if(dx > LEVEL_SIZE / 2) dx -= LEVEL_SIZE; if(dx < -LEVEL_SIZE / 2) dx += LEVEL_SIZE; return dx; } function distSq(a, b) { var c = distX(a, b); var d = a.y - b.y; return c * c + d * d; } function colliding(a, b) { return distSq(a, b) <= 4 * RADIUS * RADIUS; } // "Entities" function Meter() { this.offset = 10; this.amp = 0; this.percentage = 0; this.direction = METER_SPEED; } Meter.prototype.draw = function() { var left = (canvas.width - METER_WIDTH) / 2; var vmid = this.offset + METER_HEIGHT / 2; var third = METER_WIDTH / 3; var peak = METER_HEIGHT / 2; ctx.fillStyle = 'black'; ctx.rect(left, this.offset, METER_WIDTH, METER_HEIGHT); ctx.fill(); var cpUp = lerp(vmid, vmid + peak * this.amp, this.percentage); var cpDown = lerp(vmid, vmid - peak * this.amp, this.percentage); ctx.lineWidth = 2; ctx.strokeStyle = 'red'; ctx.beginPath(); var pos = left; ctx.moveTo(left, vmid); ctx.quadraticCurveTo(pos+third/2, cpUp, pos+third, vmid); pos += third; ctx.quadraticCurveTo(pos+third/2, cpDown, pos+third, vmid); pos += third; ctx.quadraticCurveTo(pos+third/2, cpUp, pos+third, vmid); ctx.stroke(); ctx.lineWidth = 3; ctx.strokeStyle = 'white'; ctx.beginPath(); ctx.rect(left, this.offset, METER_WIDTH, METER_HEIGHT); ctx.stroke(); }; Meter.prototype.onUpdate = function(dt) { if(this.percentage > 1) { this.direction = -METER_SPEED; } if(this.percentage < -1) { this.direction = METER_SPEED; } this.percentage += this.direction * dt; this.amp = 0.5 + lerp(0, 1, activeTarget.shimmerFactor); }; function Star(x, y) { this.x = x; this.y = y; this.alpha = 0; this.randomize(); } Star.prototype.randomize = function() { this.type = Math.floor(Math.random()*starColors.length); this.color = starColors[this.type]; }; Star.prototype.draw = function() { ctx.fillStyle = this.color; ctx.globalAlpha = this.alpha; var adjPosition = translatePoints(this); ctx.fillRect(adjPosition.x, adjPosition.y, DOT_SIZE, DOT_SIZE); var top = {x: adjPosition.x + DOT_SIZE/2, y: adjPosition.y - DOT_SIZE/2 }; var bottom = {x: adjPosition.x + DOT_SIZE/2, y: adjPosition.y + DOT_SIZE/2*3}; var left = {x: adjPosition.x - DOT_SIZE/2, y: adjPosition.y + DOT_SIZE/2 }; var right = {x: adjPosition.x + DOT_SIZE/2*3, y: adjPosition.y + DOT_SIZE/2 }; drawLeafH(left, -LEAF_SIZE, LEAF_SIZE/2); drawLeafH(right, LEAF_SIZE, LEAF_SIZE/2); drawLeafV(top, -LEAF_SIZE, LEAF_SIZE/2); drawLeafV(bottom, LEAF_SIZE, LEAF_SIZE/2); ctx.globalAlpha = 1; }; Star.prototype.onUpdate = function() { if(gotPoint) { this.randomize(); } if(this.y > canvas.height + DOT_DISTANCE * 2) { this.y -= canvas.height + DOT_DISTANCE * 4; } var newAlpha = 1.5 * activeTarget.spectrum[this.type]; var a = 0.001; if(newAlpha > this.alpha) { a = 0.9; } this.alpha = Math.min(1, activeTarget.shimmerFactor * (this.alpha * (1-a) + newAlpha * a)); }; function Dot(x, y) { this.x = x; this.y = y; } Dot.prototype.draw = function() { ctx.fillStyle = 'gray'; var adjPosition = translatePoints(this); ctx.fillRect(adjPosition.x, adjPosition.y, DOT_SIZE, DOT_SIZE); }; Dot.prototype.onUpdate = function() { if(this.y > canvas.height + DOT_DISTANCE * 2) { this.y -= canvas.height + DOT_DISTANCE * 4; } }; function Target(beat) { this.reset(); this.beat = beat; this.alpha = 0; if(points < 4) { this.alpha = ease(1-points/4); } this.rippleCounter = 0; } Target.prototype.reset = function() { this.y = -20; this.x = Math.random()*LEVEL_SIZE; }; Target.prototype.color = function(p) { return color(0, 255, 0, p*this.alpha); }; Target.prototype.draw = function() { var adjPosition = translatePoints(this); drawCircle(adjPosition, RADIUS, 3, this.color(1)); drawCircle(adjPosition, 2, 3, this.color(1)); // ripples var p = lerp(1, 0, ease(this.rippleCounter/RIPPLE_SIZE)); drawCircle(adjPosition, RADIUS+7+this.rippleCounter, 1, this.color(p)); drawCircle(adjPosition, RADIUS+4+this.rippleCounter, 1, this.color(p*0.8)); drawCircle(adjPosition, RADIUS+1+this.rippleCounter, 1, this.color(p*0.5)); }; Target.prototype.onUpdate = function(dt, t) { this.rippleCounter += RIPPLE_SPEED * dt; if(this.rippleCounter > RIPPLE_SIZE) { this.rippleCounter = 0; } if(gotPoint) { gotPoint = false; } if(colliding(player, this)) { gotPoint = true; this.beat.setPan(0); this.beat.setVolume(100); this.shouldDelete = true; return; } if(this.y > canvas.height + 20) { this.reset(); } var xDist = distX(this, player); var s = sign(xDist); var xDistAbs = easeOutExpo(Math.abs(xDist), 0, 100, LEVEL_SIZE/2); var yDist = this.y - player.y; var angle = 0; if(yDist != 0) { angle = easeOutExpo(Math.abs(Math.atan2(Math.abs(yDist), xDist) - Math.PI/2), 0, 100, Math.PI); } else { angle = s * 100; } this.beat.setPan(Math.max(xDistAbs, angle) * s); var rDist = xDist*xDist + yDist*yDist; if(rDist >= 250000) { rDist = 250000; } rDist = 250000 - rDist; rDist /= 2500; this.beat.setVolume(rDist); this.shimmerFactor = easeOutExpo(Math.abs(xDist), 1, -1, LEVEL_SIZE/2) * rDist / 100; if(isNaN(this.shimmerFactor) || this.shimmerFactor < 0) { this.shimmerFactor = 0; } this.spectrum = this.beat.getSpectrum(t*1000); }; var player = { x: 0, y: canvas.height - 20, dx: PLAYER_SPEED }; var meter = new Meter(); var points; var entities; var activeTarget; var respawnTarget = function() { activeTarget = new Target(); entities.push(activeTarget); }; var reset = function() { points = 0; entities = []; for(var j = 0; j < canvas.height / DOT_DISTANCE + 4; ++j) { for(var i = 0; i < LEVEL_SIZE / DOT_DISTANCE; ++i) { var x = i * DOT_DISTANCE; var y = (j-4) * DOT_DISTANCE; if(j % 2 == 0 && i % 2 == 0) { entities.push(new Dot(x, y)); } if(j % 4 == 1) { if(i % 4 == 0) { entities.push(new Star(x, y)); } else { entities.push(new Dot(x, y)); } } if(j % 4 == 3) { if(i % 4 == 2) { entities.push(new Star(x, y)); } else { entities.push(new Dot(x, y)); } } } } respawnTarget(); }; reset(); // Update var update = function(dt) { if(paused) return; var repeat = false; beatTime += dt; while(beatTime > 4) { repeat = true; beatTime -= 4; } if(repeat) { currentBeats.map(function(beat) { beat.play(); }); } if(37 in keysDown) { player.x -= player.dx * dt; } if(39 in keysDown) { player.x += player.dx * dt; } player.x = (player.x + LEVEL_SIZE) % LEVEL_SIZE; for(var i = entities.length - 1; i >= 0; --i) { var entity = entities[i]; entity.y += STAGE_SPEED * dt; entity.onUpdate(dt, beatTime); if(entity.shouldDelete) { entities.splice(i, 1); } } meter.onUpdate(dt); if(gotPoint) { ++points; soundManager.play('get'); if(beats.length > 0) { respawnTarget(); switchBeat(points % beats.length); } } }; // Render var translatePoints = function(point) { var x = point.x - player.x + canvas.width/2; if(x < -ALLOWANCE) x += LEVEL_SIZE; if(x > LEVEL_SIZE - ALLOWANCE) x -= LEVEL_SIZE; return {x:x, y:point.y}; }; var render = function() { ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); entities.map(function(elem) { elem.draw(); }); drawPlayer(translatePoints(player)); meter.draw(); ctx.fillStyle = "white"; ctx.fillText(points+"", 5, 15); if(debug) { drawSpectrum(); } if(paused) { ctx.textAlign = 'center'; ctx.fillText("Paused (P to unpause)", canvas.width / 2, canvas.height / 2); ctx.textAlign = 'start'; } }; var main = function() { var now = Date.now(); var delta = (now - then); delta /= 1000; update(delta); render(); then = now; }; var then; var start = function() { then = Date.now(); setInterval(main, 10); }; var pause = function() { paused = !paused; if(paused) { currentBeats.map(function(beat) {beat.pause()}); } else { currentBeats.map(function(beat) {beat.resume()}); } }; ctx.textAlign = 'center'; ctx.fillText("Loading...", canvas.width / 2, canvas.height / 2); ctx.textAlign = 'start'; Q.all([soundPromise]).done(function() { switchBeat(0); start(); });