Browse Source

Add scene manager

Thomas Dy 11 years ago
parent
commit
ce66f96946
4 changed files with 648 additions and 557 deletions
  1. 2 0
      index.html
  2. 110 557
      scripts/game.js
  3. 508 0
      scripts/games/safari.js
  4. 28 0
      scripts/util.js

+ 2 - 0
index.html

@@ -27,6 +27,8 @@
     <script type="text/javascript" src="scripts/q.js"></script>
     <script type="text/javascript" src="scripts/state-machine.js"></script>
 		<script type="text/javascript" src="scripts/soundmanager2.js"></script>
+    <script type="text/javascript" src="scripts/util.js"></script>
+    <script type="text/javascript" src="scripts/games/safari.js"></script>
 		<script type="text/javascript" src="scripts/game.js"></script>
 	</body>
 </html>

+ 110 - 557
scripts/game.js

@@ -1,573 +1,126 @@
-function createSound(name, url) {
-  var deferred = Q.defer();
-  soundManager.createSound({
-    id: name,
-    url: url,
-    autoLoad: true,
-    onload: function() {
-      deferred.resolve(this);
-    }
-  });
-  return deferred.promise;
-}
-
-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)+')';
-}
-
-var SoundSafari = function(canvas, beatInfo, pSoundManager) {
-  var ctx = canvas.getContext('2d');
-
-  // 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;
-
-  var state = StateMachine.create({
-    initial: 'waiting',
-    events: [
-      {name: 'play', from: 'waiting', to: 'playing'},
-      {name: 'point', from: 'playing', to: 'waiting'}
-    ],
-    callbacks: {
-      onpoint: function() {
-        ++points;
-        soundManager.play('get');
-        switchBeat(points % beats.length);
-      }
-    }
-  });
-
-  var starColors = [
-    'maroon',
-    'red',
-    'orange',
-    'yellow',
-    'olive',
-    'green',
-    'teal',
-    'blue',
-    'navy',
-    'purple'
-  ];
-
-  var paused = false;
-  var debug = false;
-
-  var beats = [];
-  var gameTime = 0;
-  var currentBeats = [];
-
-  var switchBeat = function(index) {
-    var currentBeat = beats[index];
-    currentBeats.push(currentBeat);
-    if(currentBeats.length > 4) {
-      currentBeats.splice(0, 1);
+var Game = {
+  canvas: document.getElementById('game'),
+  keysDown: {},
+  start: function() {
+    Game.sceneManager.push(MainMenu);
+    this.then = Date.now();
+    this.time = 0;
+    setInterval(function() {
+      Game.loop();
+    }, 10);
+  },
+  loop: function() {
+    var now = Date.now();
+    var delta = now - this.then;
+    this.time += delta;
+    this.sceneManager.update(delta/1000);
+    this.sceneManager.draw();
+    this.then = now;
+  },
+  pause: function() {
+    Game.sceneManager.push(PauseScreen);
+  },
+  sceneManager: {
+    sceneStack: [],
+    currentScene: null,
+    update: function(delta) {
+      if(this.currentScene) this.currentScene.update(delta);
+    },
+    draw: function() {
+      if(this.currentScene) this.currentScene.draw();
+    },
+    push: function(scene) {
+      if(this.currentScene) this.currentScene.pause();
+      this.sceneStack.push(scene);
+      scene.load();
+      this.currentScene = scene;
+    },
+    pop: function() {
+      var scene = this.sceneStack.pop();
+      scene.unload();
+      this.currentScene = this.sceneStack[this.sceneStack.length-1];
+      this.currentScene.resume();
     }
+  }
+};
 
-    activeTarget = new Target(currentBeat);
-    entities.push(activeTarget);
-  };
-
-  var soundPromise = pSoundManager.then(function() {
-    var soundPromises = beatInfo.map(function(elem, index) {
-      var beat = { info: elem, rounds: 0 };
-      var promise = createSound('beat'+index, elem.url).then(function(sound) {
-        beat.sound = sound;
-      });
-      beats.push(beat);
-      return promise;
-    });
-    soundPromises.push(createSound('get', 'sound/get.mp3'));
-
-    return Q.all(soundPromises);
-  });
-
-  // Input handling
-  var keysDown = {};
+(function() {
+  Game.context = Game.canvas.getContext('2d');
 
   addEventListener('keydown', function(e) {
-    keysDown[e.keyCode] = true;
+    Game.keysDown[e.keyCode] = true;
   }, false);
 
   addEventListener('keyup', function(e) {
-    if(e.keyCode == 68) {
-      debug = !debug;
-    }
-    if(e.keyCode == 80) {
-      pause();
-    }
-    delete keysDown[e.keyCode];
+    delete Game.keysDown[e.keyCode];
   }, false);
 
-  addEventListener('blur', function() {
-    if(!paused) pause();
-  });
-
-  // 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);
+  var soundDeferred = Q.defer();
+  soundManager.setup({
+    url: 'swf/',
+    flashVersion: 9,
+    onready: function() {
+      soundDeferred.resolve(this);
     }
+  });
+  soundDeferred.promise.done(function() {
+    Game.start();
+  });
+})();
+
+var MainMenu = {
+  update: function() {},
+  draw: function() {
+    Game.context.fillStyle = 'black';
+    Game.context.fillRect(0, 0, Game.canvas.width, Game.canvas.height);
+    Game.context.fillStyle = 'white';
+    Game.context.textAlign = 'center';
+    Game.context.fillText('Audventure', Game.canvas.width/2, Game.canvas.height/2 - 20);
+    Game.context.fillText('Press any key to start', Game.canvas.width/2, Game.canvas.height/2 + 20);
   }
+};
 
-  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();
+(function() {
+  function onKeyUp() {
+    var beats = [
+      {url: 'sound/b1.mp3', duration: 4000},
+      {url: 'sound/b2.mp3', duration: 4000},
+      {url: 'sound/b3.mp3', duration: 4000},
+      {url: 'sound/b4.mp3', duration: 4000},
+      {url: 'sound/b5.mp3', duration: 4000}
+    ];
 
-    ctx.lineWidth = 3;
-    ctx.strokeStyle = 'white';
-    ctx.beginPath();
-    ctx.rect(left, this.offset, METER_WIDTH, METER_HEIGHT);
-    ctx.stroke();
+    Game.sceneManager.push(new SoundSafari(beats));
   };
-
-  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();
+  MainMenu.load = MainMenu.resume = function() {
+    addEventListener('keyup', onKeyUp);
   }
-
-  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 = 1.5 * activeTarget.spectrum[this.type];
-      var a = 0.9;
-      if(newAlpha > this.alpha) {
-        a = 0.9;
-      }
-      this.alpha = Math.min(1, activeTarget.shimmerFactor * (this.alpha * (1-a) + newAlpha * a));
-    }
-    else if(state.current == 'waiting') {
-      this.alpha = this.alpha * 0.95;
-    }
-  };
-
-  function Dot(x, y) {
-    this.x = x;
-    this.y = y;
+  MainMenu.unload = MainMenu.pause = function() {
+    removeEventListener('keyup', onKeyUp);
   }
-
-  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.alpha = 0;
-    if(points < 4) {
-      this.alpha = ease(1-points/4);
-    }
-    this.rippleCounter = 0;
-    this.reset();
-  }
-
-  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(colliding(player, this)) {
-      state.point();
-      this.beat.sound.setPan(0);
-      this.beat.sound.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.sound.setPan(Math.max(xDistAbs, angle) * s);
-
-    var rDist = xDist*xDist + yDist*yDist;
-    if(rDist >= 250000) {
-      rDist = 250000;
-    }
-    rDist = 250000 - rDist;
-    rDist /= 2500;
-    this.beat.sound.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;
-    }
-    var pos = t%this.beat.info.duration;
-    if(pos < this.beat.sound.duration) this.spectrum = this.beat.sound.getSpectrum(pos);
-  };
-
-  var player = {
-    x: 0,
-    y: canvas.height - 20,
-    dx: PLAYER_SPEED
-  };
-
-  var meter = new Meter();
-
-  var points;
-  var entities;
-  var 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));
-          }
-        }
-      }
-    }
-  };
-
-  // Update
-  var update = function(delta) {
-    if(paused) return;
-    var dt = delta / 1000;
-
-    currentBeats.map(function(beat) {
-      if(beat.repeat) {
-        beat.repeat = false;
-      }
-      else {
-        if(gameTime + delta > Math.ceil(gameTime / beat.info.duration) * beat.info.duration) {
-          beat.repeat = true;
-        }
-      }
-    });
-
-    currentBeats.map(function(beat) {
-      if(beat.repeat) {
-        if(beat == activeTarget.beat && state.current == 'waiting') {
-          state.play();
-        }
-        beat.sound.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, gameTime);
-      if(entity.shouldDelete) {
-        entities.splice(i, 1);
-      }
-    }
-    meter.onUpdate(dt);
-
-    gameTime += delta;
-  };
-
-  // 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.fillStyle = 'white';
-      ctx.textAlign = 'center';
-      ctx.fillText("Paused (P to unpause)", canvas.width / 2, canvas.height / 2);
-      ctx.textAlign = 'start';
-    }
-  };
-
-  var then;
-
-  var main = function() {
-    var now = Date.now();
-    var delta = (now - then);
-    update(delta);
-    render();
-    then = now;
-  };
-
-  var start = function() {
-    reset();
-    switchBeat(0);
-    then = Date.now();
-    setInterval(main, 10);
-  };
-
-  var pause = function() {
-    paused = !paused;
-    if(paused) {
-      currentBeats.map(function(beat) {beat.sound.pause()});
-    }
-    else {
-      currentBeats.map(function(beat) {beat.sound.resume()});
-    }
-  };
-
-  ctx.textAlign = 'center';
-  ctx.fillText("Loading...", canvas.width / 2, canvas.height / 2);
-  ctx.textAlign = 'start';
-
-  Q.all([soundPromise]).done(function() {
-    start();
-  });
-};
-
-var canvas = document.getElementById('game');
-
-var beats = [
-  {url: 'sound/b1.mp3', duration: 4000},
-  {url: 'sound/b2.mp3', duration: 4000},
-  {url: 'sound/b3.mp3', duration: 4000},
-  {url: 'sound/b4.mp3', duration: 4000},
-  {url: 'sound/b5.mp3', duration: 4000}
-];
-
-var soundDeferred = Q.defer();
-var pSoundManager = soundDeferred.promise;
-soundManager.setup({
-  url: 'swf/',
-  flashVersion: 9,
-  onready: function() {
-    soundDeferred.resolve(this);
-  }
-});
-
-SoundSafari(canvas, beats, pSoundManager);
-
+})();
+
+var PauseScreen = {
+  onKeyUp: function(e) {
+    if(e.keyCode == 80) Game.sceneManager.pop();
+    if(e.keyCode == 81) {
+      Game.sceneManager.pop();
+      Game.sceneManager.pop();
+    }
+  },
+  load: function() {addEventListener('keyup', this.onKeyUp)},
+  unload: function() {removeEventListener('keyup', this.onKeyUp)},
+  update: function() {},
+  draw: function() {
+    Game.context.fillStyle = 'black';
+    Game.context.fillRect(0, 0, Game.canvas.width, Game.canvas.height);
+    Game.context.fillStyle = 'white';
+    Game.context.textAlign = 'center';
+    Game.context.fillText('Paused', Game.canvas.width/2, Game.canvas.height/2 - 30);
+    Game.context.fillText('P to unpause', Game.canvas.width/2, Game.canvas.height/2);
+    Game.context.fillText('Q to quit', Game.canvas.width/2, Game.canvas.height/2 + 20);
+
+  },
+  pause: this.unload,
+  resume: this.load
+};

+ 508 - 0
scripts/games/safari.js

@@ -0,0 +1,508 @@
+function SoundSafari(beatInfo) {
+  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 RIPPLE_SIZE = 10;
+  var RIPPLE_SPEED = 10;
+  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'}
+    ],
+    callbacks: {
+      onready: function() {
+        reset();
+      },
+      onpoint: function() {
+        ++points;
+        soundManager.play('get');
+        switchBeat(points % beats.length);
+      }
+    }
+  });
+
+  var gameTime = 0;
+  var beats = [];
+  var currentBeats = [];
+
+  // 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);
+      }
+    }
+  }
+
+  // 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 = 1.5 * activeTarget.spectrum[this.type];
+        var a = 0.9;
+        this.alpha = Math.min(1, activeTarget.shimmerFactor * (this.alpha * (1-a) + newAlpha * a));
+      }
+      else if(state.current == 'waiting') {
+        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.alpha = 0;
+      this.spectrum = [];
+      if(points < 4) {
+        this.alpha = ease(1-points/4);
+      }
+      this.rippleCounter = 0;
+      this.reset();
+    }
+
+    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) {
+      this.rippleCounter += RIPPLE_SPEED * dt;
+      if(this.rippleCounter > RIPPLE_SIZE) {
+        this.rippleCounter = 0;
+      }
+
+      if(colliding(player, this)) {
+        state.point();
+        this.beat.sound.setPan(0);
+        this.beat.sound.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.sound.setPan(Math.max(xDistAbs, angle) * s);
+
+      var rDist = xDist*xDist + yDist*yDist;
+      if(rDist >= 250000) {
+        rDist = 250000;
+      }
+      rDist = 250000 - rDist;
+      rDist /= 2500;
+      this.beat.sound.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;
+      }
+      var pos = gameTime % this.beat.info.duration;
+      if(pos < this.beat.sound.duration) this.spectrum = this.beat.sound.getSpectrum(pos);
+    };
+  }
+
+  var player = {
+    x: 0,
+    y: canvas.height - 20,
+    dx: PLAYER_SPEED
+  };
+
+  var meter = new Meter();
+
+  var points;
+  var entities;
+  var activeTarget;
+
+  function switchBeat(index) {
+    var currentBeat = beats[index];
+    currentBeats.push(currentBeat);
+    if(currentBeats.length > 4) {
+      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;
+
+    currentBeats.map(function(beat) {
+      if(beat.repeat) {
+        beat.repeat = false;
+      }
+      else {
+        if(gameTime + delta > nextPlayTime(beat.sound.duration)) {
+          beat.repeat = true;
+        }
+      }
+    });
+
+    currentBeats.map(function(beat) {
+      if(beat.repeat) {
+        if(beat == activeTarget.beat && state.current == 'waiting') {
+          state.play();
+        }
+        beat.sound.play();
+      }
+    });
+
+    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() {
+    ctx.fillStyle = "black";
+    ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+    if(state.current == 'loading') {
+      ctx.fillStyle = 'white';
+      ctx.textAlign = 'center';
+      ctx.fillText("Loading...", canvas.width / 2, canvas.height / 2);
+      ctx.textAlign = 'start';
+    }
+    else {
+      entities.map(function(elem) {
+        elem.draw();
+      });
+      drawPlayer(translatePoints(player));
+      meter.draw();
+      ctx.fillStyle = "white";
+      ctx.fillText(points+"", 5, 15);
+
+      if(debug) {
+        drawSpectrum();
+      }
+    }
+  };
+
+  var onKeyUp = function(e) {
+    if(e.keyCode == 80) {
+      Game.pause();
+    }
+    else if(e.keyCode == 68) {
+      debug = !debug;
+    }
+  };
+
+  this.load = function() {
+    addEventListener('keyup', onKeyUp);
+    beats = [];
+    var soundPromises = beatInfo.map(function(elem, index) {
+      var beat = { info: elem, rounds: 0 };
+      var promise = createSound('beat'+index, elem.url).then(function(sound) {
+        beat.sound = sound;
+      });
+      beats.push(beat);
+      return promise;
+    });
+    soundPromises.push(createSound('get', 'sound/get.mp3'));
+    Q.all(soundPromises).done(function() {
+      state.ready();
+    })
+  };
+
+  this.unload = function() {
+    removeEventListener('keyup', onKeyUp);
+    soundManager.destroySound('get');
+    beats.forEach(function(beat) {beat.sound.destruct()});
+  };
+
+  this.pause = function() {
+    removeEventListener('keyup', onKeyUp);
+    currentBeats.map(function(beat) {beat.sound.pause()});
+  };
+
+  this.resume = function() {
+    addEventListener('keyup', onKeyUp);
+    currentBeats.map(function(beat) {beat.sound.resume()});
+  };
+};

+ 28 - 0
scripts/util.js

@@ -0,0 +1,28 @@
+function createSound(name, url) {
+  var deferred = Q.defer();
+  soundManager.createSound({
+    id: name,
+    url: url,
+    autoLoad: true,
+    onload: function() {
+      deferred.resolve(this);
+    }
+  });
+  return deferred.promise;
+}
+
+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)+')';
+}