function SoundSafari(beatInfo, endless) { var canvas = Game.canvas; var ctx = Game.context; // 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 DOT_SIZE = 3; var DOT_DISTANCE = 4 * 5; var starColors = [ 'maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'teal', 'blue', 'navy', 'purple' ]; var debug = false; var state = StateMachine.create({ initial: 'loading', events: [ {name: 'ready', from: 'loading', to: 'waiting'}, {name: 'play', from: 'waiting', to: 'playing'}, {name: 'point', from: 'playing', to: 'waiting'}, {name: 'win', from: 'waiting', to: 'ending'}, {name: 'end', from: 'ending', to: 'done'} ], callbacks: { onready: function() { reset(); }, onend: function() { Game.sceneManager.pop(); }, onpoint: function() { ++points; sfxManager.play('get'); if(!endless && points >= beats.length) { state.win(); } else { switchBeat(points % beats.length); } } } }); var gameTime = 0; var beats = []; var currentBeats = []; // Setup sound pipeline var analyser = audioCtx.createAnalyser(); analyser.fftSize = 32; var spectrum = new Uint8Array(analyser.frequencyBinCount); var panner = audioCtx.createPanner(); panner.panningModel = 'HRTF'; panner.distanceModel = 'exponential'; panner.refDistance = 50; panner.rolloffFactor = 1.2; panner.setOrientation(0, 0, 1); panner.coneInnerAngle = 60; panner.coneOuterAngle = 60; panner.coneOuterGain = 0.7; audioCtx.listener.setOrientation(0,0,-1,0,1,0); analyser.connect(panner); panner.connect(audioCtx.destination); // Drawing functions { 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 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, spectrum.length * (barWidth+barSpacing) + 2 * ctx.lineWidth - barSpacing, barHeight + 2 * ctx.lineWidth ); ctx.stroke(); for(var i = 0; i < spectrum.length; ++i) { var height = spectrum[i] / 255 * 20; ctx.fillStyle = starColors[i]; ctx.fillRect(i * (barWidth + barSpacing) + margin, canvas.height - height - margin, barWidth, height); } } } // Helpers { 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; } function translatePoints(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}; } function nextPlayTime(duration) { return Math.ceil(gameTime / duration) * duration; } } // "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(this.y > canvas.height + DOT_DISTANCE * 2) { this.y -= canvas.height + DOT_DISTANCE * 4; this.randomize(); } if(state.current == 'playing') { var newAlpha = spectrum[this.type] / 255 * 1.5; var a = 0.9; this.alpha = Math.min(1, activeTarget.shimmerFactor * (this.alpha * (1-a) + newAlpha * a)); } else if(state.current == 'waiting' || state.current == 'ending') { this.alpha = this.alpha * 0.95; } }; 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.beat = beat; this.spectrum = []; this.alpha = 0; if(points < 4) { this.alpha = ease(1-points/4); } this.init(0, 0, color(0, 255, 0, this.alpha)); this.reset(); } Target.prototype = new Beater(); Target.prototype.reset = function() { this.y = -20; this.x = Math.random()*LEVEL_SIZE; }; Target.prototype.draw = function() { this.position = translatePoints(this); Beater.prototype.draw.call(this); }; Target.prototype.onUpdate = function(dt) { this.update(dt); if(colliding(player, this)) { state.point(); this.beat.sound.setTarget(audioCtx.destination); 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; } panner.setPosition(xDist, 0, yDist); var rDist = xDist*xDist + yDist*yDist; if(rDist >= 250000) { rDist = 250000; } rDist = 250000 - rDist; rDist /= 2500; this.shimmerFactor = easeOutExpo(Math.abs(xDist), 1, -1, LEVEL_SIZE/2) * rDist / 100; if(isNaN(this.shimmerFactor) || this.shimmerFactor < 0) { this.shimmerFactor = 0; } analyser.getByteFrequencyData(spectrum); }; } var player = { x: 0, y: canvas.height - 20, dx: PLAYER_SPEED }; var reference = { x: canvas.width / 2, y: canvas.height - 20 }; var meter = new Meter(); var points; var entities; var activeTarget; function switchBeat(index) { currentBeats.forEach(beat => beat.sound.setTarget(audioCtx.destination)); var currentBeat = beats[index]; currentBeat.sound.setTarget(analyser); currentBeats.push(currentBeat); if(currentBeats.length > Math.min(4, beats.length)) { currentBeats.splice(0, 1); } activeTarget = new Target(currentBeat); entities.push(activeTarget); } function reset() { 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)); } } } } switchBeat(0); } // Update this.update = function(dt) { if(state.current == 'loading') return; var delta = dt * 1000; for(var i = currentBeats.length-1; i >= 0; --i) { var beat = currentBeats[i]; if(gameTime + delta > nextPlayTime(beat.info.duration)) { if(state.current == 'ending' && i == 0) { currentBeats.splice(0, 1); continue; } if(beat == activeTarget.beat && state.current == 'waiting') { state.play(); } beat.sound.play(); } } if(currentBeats.length == 0 && state.can('end')) { state.end(); } if(37 in Game.keysDown) { player.x -= player.dx * dt; } if(39 in Game.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); if(entity.shouldDelete) { entities.splice(i, 1); } } meter.onUpdate(dt); gameTime += delta; }; this.draw = function() { if(state.current != 'loading') { ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); entities.map(function(elem) { elem.draw(); }); // player drawCircle(reference, RADIUS, 3, 'white'); meter.draw(); ctx.fillStyle = "white"; ctx.fillText(points+"", 5, 15); if(debug) { drawSpectrum(); } } if(this.coverAlphaTween.promise.isPending()) { ctx.globalAlpha = this.coverAlphaTween.value; ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.globalAlpha = 1; } if(state.current == 'loading') { ctx.globalAlpha = this.textAlphaTween.value; ctx.fillStyle = 'white'; ctx.textAlign = 'center'; ctx.font = font(48); ctx.fillText("sound safari", canvas.width / 2, canvas.height / 2); ctx.font = font(12); ctx.textAlign = 'start'; ctx.globalAlpha = 1; } }; var onKeyUp = function(e) { switch(e.charCode) { case 80: // P case 112: // p case 32: // space Game.pause(); sfxManager.play('pause'); break; case 68: // D debug = !debug; break; } }; this.load = function() { addEventListener('keypress', onKeyUp); beats = []; var soundPromises = beatInfo.map(function(elem, index) { var beat = { info: elem, rounds: 0 }; var promise = loadSound(elem.url).then(function(sound) { beat.sound = sound; }); beats.push(beat); return promise; }); soundPromises.push(sfxManager.loadSound('get', 'sound/get.mp3')); this.textAlphaTween = this.tween(0) .wait(0.5) .to(1, 1.5) .wait(1) .waitFor(Q.all(soundPromises)) .to(0, 1.5) .animate(); this.coverAlphaTween = this.tween(1) .waitFor(this.textAlphaTween.promise) .to(0, 1) .animate(); this.textAlphaTween.promise.done(function() { state.ready(); }); }; this.unload = function() { removeEventListener('keypress', onKeyUp); sfxManager.unloadSound('get'); }; this.pause = function() { removeEventListener('keypress', onKeyUp); currentBeats.map(function(beat) {beat.sound.pause()}); }; this.resume = function() { addEventListener('keypress', onKeyUp); currentBeats.map(function(beat) {beat.sound.resume()}); }; } SoundSafari.prototype = new Scene();