game.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. function createSound(name, url) {
  2. var deferred = Q.defer();
  3. soundManager.createSound({
  4. id: name,
  5. url: url,
  6. autoLoad: true,
  7. onload: function() {
  8. deferred.resolve(this);
  9. }
  10. });
  11. return deferred.promise;
  12. }
  13. function sign(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; }
  14. function lerp(from, to, p) {
  15. return to * p + from * (1 - p);
  16. }
  17. function ease(v) { return v * v * (3 - 2 * v); }
  18. function easeOutExpo(t, b, c, d) {
  19. return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
  20. }
  21. function color(r,g,b,a) {
  22. return 'rgba('+r+','+g+','+b+','+a.toFixed(5)+')';
  23. }
  24. var SoundSafari = function(canvas, beatInfo, pSoundManager) {
  25. var ctx = canvas.getContext('2d');
  26. // Constants
  27. var RADIUS = 8;
  28. var LEAF_SIZE = 8;
  29. var LEVEL_SIZE = 800;
  30. var ALLOWANCE = (LEVEL_SIZE - canvas.width)/2;
  31. var METER_WIDTH = 120;
  32. var METER_HEIGHT = 40;
  33. var METER_SPEED = 10;
  34. var PLAYER_SPEED = 50;
  35. var STAGE_SPEED = 25;
  36. var RIPPLE_SIZE = 10;
  37. var RIPPLE_SPEED = 10;
  38. var DOT_SIZE = 3;
  39. var DOT_DISTANCE = 4 * 5;
  40. var starColors = [
  41. 'maroon',
  42. 'red',
  43. 'orange',
  44. 'yellow',
  45. 'olive',
  46. 'green',
  47. 'teal',
  48. 'blue',
  49. 'navy',
  50. 'purple'
  51. ];
  52. var paused = false;
  53. var debug = false;
  54. var beats = [];
  55. var gameTime = 0;
  56. var currentBeats = [];
  57. var gotPoint = false;
  58. var switchBeat = function(index) {
  59. var currentBeat = beats[index];
  60. currentBeats.push(currentBeat);
  61. if(currentBeats.length > 4) {
  62. currentBeats.splice(0, 1);
  63. }
  64. activeTarget = new Target(currentBeat);
  65. entities.push(activeTarget);
  66. };
  67. var soundPromise = pSoundManager.then(function() {
  68. var soundPromises = beatInfo.map(function(elem, index) {
  69. var beat = { info: elem, rounds: 0 };
  70. var promise = createSound('beat'+index, elem.url).then(function(sound) {
  71. beat.sound = sound;
  72. });
  73. beats.push(beat);
  74. return promise;
  75. });
  76. soundPromises.push(createSound('get', 'sound/get.mp3'));
  77. return Q.all(soundPromises);
  78. });
  79. // Input handling
  80. var keysDown = {};
  81. addEventListener('keydown', function(e) {
  82. keysDown[e.keyCode] = true;
  83. }, false);
  84. addEventListener('keyup', function(e) {
  85. if(e.keyCode == 68) {
  86. debug = !debug;
  87. }
  88. if(e.keyCode == 80) {
  89. pause();
  90. }
  91. delete keysDown[e.keyCode];
  92. }, false);
  93. addEventListener('blur', function() {
  94. if(!paused) pause();
  95. });
  96. // Drawing functions
  97. function drawCircle(point, r, w, color) {
  98. ctx.beginPath();
  99. ctx.arc(point.x, point.y, r, 0, 2*Math.PI, false);
  100. ctx.lineWidth = w;
  101. ctx.strokeStyle = color;
  102. ctx.stroke();
  103. }
  104. function drawLeafV(point, h, w) {
  105. ctx.beginPath();
  106. ctx.moveTo(point.x, point.y);
  107. ctx.quadraticCurveTo(point.x + w, point.y + h/2, point.x, point.y + h);
  108. ctx.quadraticCurveTo(point.x - w, point.y + h/2, point.x, point.y);
  109. ctx.fill();
  110. }
  111. function drawLeafH(point, w, h) {
  112. ctx.beginPath();
  113. ctx.moveTo(point.x, point.y);
  114. ctx.quadraticCurveTo(point.x + w/2, point.y + h, point.x + w, point.y);
  115. ctx.quadraticCurveTo(point.x + w/2, point.y - h, point.x, point.y);
  116. ctx.fill();
  117. }
  118. function drawPlayer(point) {
  119. drawCircle(point, RADIUS, 5, "#FFFFFF");
  120. }
  121. function drawSpectrum() {
  122. var barHeight = 30;
  123. var barWidth = 6;
  124. var barSpacing = 2;
  125. var margin = 5;
  126. ctx.strokeStyle = 'white';
  127. ctx.lineWidth = 2;
  128. ctx.beginPath();
  129. ctx.rect(
  130. margin - ctx.lineWidth,
  131. canvas.height - barHeight - ctx.lineWidth - margin,
  132. activeTarget.spectrum.length * (barWidth+barSpacing) + 2 * ctx.lineWidth - barSpacing,
  133. barHeight + 2 * ctx.lineWidth
  134. );
  135. ctx.stroke();
  136. for(var i = 0; i < activeTarget.spectrum.length; ++i) {
  137. var height = activeTarget.spectrum[i]*20;
  138. ctx.fillStyle = starColors[i];
  139. ctx.fillRect(i * (barWidth + barSpacing) + margin, canvas.height - height - margin, barWidth, height);
  140. }
  141. }
  142. function distX(a, b) {
  143. var dx = a.x - b.x;
  144. if(dx > LEVEL_SIZE / 2) dx -= LEVEL_SIZE;
  145. if(dx < -LEVEL_SIZE / 2) dx += LEVEL_SIZE;
  146. return dx;
  147. }
  148. function distSq(a, b) {
  149. var c = distX(a, b);
  150. var d = a.y - b.y;
  151. return c * c + d * d;
  152. }
  153. function colliding(a, b) {
  154. return distSq(a, b) <= 4 * RADIUS * RADIUS;
  155. }
  156. // "Entities"
  157. function Meter() {
  158. this.offset = 10;
  159. this.amp = 0;
  160. this.percentage = 0;
  161. this.direction = METER_SPEED;
  162. }
  163. Meter.prototype.draw = function() {
  164. var left = (canvas.width - METER_WIDTH) / 2;
  165. var vmid = this.offset + METER_HEIGHT / 2;
  166. var third = METER_WIDTH / 3;
  167. var peak = METER_HEIGHT / 2;
  168. ctx.fillStyle = 'black';
  169. ctx.rect(left, this.offset, METER_WIDTH, METER_HEIGHT);
  170. ctx.fill();
  171. var cpUp = lerp(vmid, vmid + peak * this.amp, this.percentage);
  172. var cpDown = lerp(vmid, vmid - peak * this.amp, this.percentage);
  173. ctx.lineWidth = 2;
  174. ctx.strokeStyle = 'red';
  175. ctx.beginPath();
  176. var pos = left;
  177. ctx.moveTo(left, vmid);
  178. ctx.quadraticCurveTo(pos+third/2, cpUp, pos+third, vmid);
  179. pos += third;
  180. ctx.quadraticCurveTo(pos+third/2, cpDown, pos+third, vmid);
  181. pos += third;
  182. ctx.quadraticCurveTo(pos+third/2, cpUp, pos+third, vmid);
  183. ctx.stroke();
  184. ctx.lineWidth = 3;
  185. ctx.strokeStyle = 'white';
  186. ctx.beginPath();
  187. ctx.rect(left, this.offset, METER_WIDTH, METER_HEIGHT);
  188. ctx.stroke();
  189. };
  190. Meter.prototype.onUpdate = function(dt) {
  191. if(this.percentage > 1) {
  192. this.direction = -METER_SPEED;
  193. }
  194. if(this.percentage < -1) {
  195. this.direction = METER_SPEED;
  196. }
  197. this.percentage += this.direction * dt;
  198. this.amp = 0.5 + lerp(0, 1, activeTarget.shimmerFactor);
  199. };
  200. function Star(x, y) {
  201. this.x = x;
  202. this.y = y;
  203. this.alpha = 0;
  204. this.randomize();
  205. }
  206. Star.prototype.randomize = function() {
  207. this.type = Math.floor(Math.random()*starColors.length);
  208. this.color = starColors[this.type];
  209. };
  210. Star.prototype.draw = function() {
  211. ctx.fillStyle = this.color;
  212. ctx.globalAlpha = this.alpha;
  213. var adjPosition = translatePoints(this);
  214. ctx.fillRect(adjPosition.x, adjPosition.y, DOT_SIZE, DOT_SIZE);
  215. var top = {x: adjPosition.x + DOT_SIZE/2, y: adjPosition.y - DOT_SIZE/2 };
  216. var bottom = {x: adjPosition.x + DOT_SIZE/2, y: adjPosition.y + DOT_SIZE/2*3};
  217. var left = {x: adjPosition.x - DOT_SIZE/2, y: adjPosition.y + DOT_SIZE/2 };
  218. var right = {x: adjPosition.x + DOT_SIZE/2*3, y: adjPosition.y + DOT_SIZE/2 };
  219. drawLeafH(left, -LEAF_SIZE, LEAF_SIZE/2);
  220. drawLeafH(right, LEAF_SIZE, LEAF_SIZE/2);
  221. drawLeafV(top, -LEAF_SIZE, LEAF_SIZE/2);
  222. drawLeafV(bottom, LEAF_SIZE, LEAF_SIZE/2);
  223. ctx.globalAlpha = 1;
  224. };
  225. Star.prototype.onUpdate = function() {
  226. if(gotPoint) {
  227. this.randomize();
  228. }
  229. if(this.y > canvas.height + DOT_DISTANCE * 2) {
  230. this.y -= canvas.height + DOT_DISTANCE * 4;
  231. }
  232. var newAlpha = 1.5 * activeTarget.spectrum[this.type];
  233. var a = 0.001;
  234. if(newAlpha > this.alpha) {
  235. a = 0.9;
  236. }
  237. this.alpha = Math.min(1, activeTarget.shimmerFactor * (this.alpha * (1-a) + newAlpha * a));
  238. };
  239. function Dot(x, y) {
  240. this.x = x;
  241. this.y = y;
  242. }
  243. Dot.prototype.draw = function() {
  244. ctx.fillStyle = 'gray';
  245. var adjPosition = translatePoints(this);
  246. ctx.fillRect(adjPosition.x, adjPosition.y, DOT_SIZE, DOT_SIZE);
  247. };
  248. Dot.prototype.onUpdate = function() {
  249. if(this.y > canvas.height + DOT_DISTANCE * 2) {
  250. this.y -= canvas.height + DOT_DISTANCE * 4;
  251. }
  252. };
  253. function Target(beat) {
  254. this.beat = beat;
  255. this.alpha = 0;
  256. if(points < 4) {
  257. this.alpha = ease(1-points/4);
  258. }
  259. this.rippleCounter = 0;
  260. this.reset();
  261. }
  262. Target.prototype.reset = function() {
  263. this.y = -20;
  264. this.x = Math.random()*LEVEL_SIZE;
  265. };
  266. Target.prototype.color = function(p) {
  267. return color(0, 255, 0, p*this.alpha);
  268. };
  269. Target.prototype.draw = function() {
  270. var adjPosition = translatePoints(this);
  271. drawCircle(adjPosition, RADIUS, 3, this.color(1));
  272. drawCircle(adjPosition, 2, 3, this.color(1));
  273. // ripples
  274. var p = lerp(1, 0, ease(this.rippleCounter/RIPPLE_SIZE));
  275. drawCircle(adjPosition, RADIUS+7+this.rippleCounter, 1, this.color(p));
  276. drawCircle(adjPosition, RADIUS+4+this.rippleCounter, 1, this.color(p*0.8));
  277. drawCircle(adjPosition, RADIUS+1+this.rippleCounter, 1, this.color(p*0.5));
  278. };
  279. Target.prototype.onUpdate = function(dt, t) {
  280. this.rippleCounter += RIPPLE_SPEED * dt;
  281. if(this.rippleCounter > RIPPLE_SIZE) {
  282. this.rippleCounter = 0;
  283. }
  284. if(gotPoint) {
  285. gotPoint = false;
  286. }
  287. if(colliding(player, this)) {
  288. gotPoint = true;
  289. this.beat.sound.setPan(0);
  290. this.beat.sound.setVolume(100);
  291. this.shouldDelete = true;
  292. return;
  293. }
  294. if(this.y > canvas.height + 20) {
  295. this.reset();
  296. }
  297. var xDist = distX(this, player);
  298. var s = sign(xDist);
  299. var xDistAbs = easeOutExpo(Math.abs(xDist), 0, 100, LEVEL_SIZE/2);
  300. var yDist = this.y - player.y;
  301. var angle = 0;
  302. if(yDist != 0) {
  303. angle = easeOutExpo(Math.abs(Math.atan2(Math.abs(yDist), xDist) - Math.PI/2), 0, 100, Math.PI);
  304. }
  305. else {
  306. angle = s * 100;
  307. }
  308. this.beat.sound.setPan(Math.max(xDistAbs, angle) * s);
  309. var rDist = xDist*xDist + yDist*yDist;
  310. if(rDist >= 250000) {
  311. rDist = 250000;
  312. }
  313. rDist = 250000 - rDist;
  314. rDist /= 2500;
  315. this.beat.sound.setVolume(rDist);
  316. this.shimmerFactor = easeOutExpo(Math.abs(xDist), 1, -1, LEVEL_SIZE/2) * rDist / 100;
  317. if(isNaN(this.shimmerFactor) || this.shimmerFactor < 0) {
  318. this.shimmerFactor = 0;
  319. }
  320. this.spectrum = this.beat.sound.getSpectrum(t%this.beat.info.duration);
  321. };
  322. var player = {
  323. x: 0,
  324. y: canvas.height - 20,
  325. dx: PLAYER_SPEED
  326. };
  327. var meter = new Meter();
  328. var points;
  329. var entities;
  330. var activeTarget;
  331. var reset = function() {
  332. points = 0;
  333. entities = [];
  334. for(var j = 0; j < canvas.height / DOT_DISTANCE + 4; ++j) {
  335. for(var i = 0; i < LEVEL_SIZE / DOT_DISTANCE; ++i) {
  336. var x = i * DOT_DISTANCE;
  337. var y = (j-4) * DOT_DISTANCE;
  338. if(j % 2 == 0 && i % 2 == 0) {
  339. entities.push(new Dot(x, y));
  340. }
  341. if(j % 4 == 1) {
  342. if(i % 4 == 0) {
  343. entities.push(new Star(x, y));
  344. }
  345. else {
  346. entities.push(new Dot(x, y));
  347. }
  348. }
  349. if(j % 4 == 3) {
  350. if(i % 4 == 2) {
  351. entities.push(new Star(x, y));
  352. }
  353. else {
  354. entities.push(new Dot(x, y));
  355. }
  356. }
  357. }
  358. }
  359. };
  360. // Update
  361. var update = function(delta) {
  362. if(paused) return;
  363. var dt = delta / 1000;
  364. currentBeats.map(function(beat) {
  365. if(beat.repeat) {
  366. beat.repeat = false;
  367. }
  368. else {
  369. if(gameTime + delta > Math.ceil(gameTime / beat.info.duration) * beat.info.duration) {
  370. beat.repeat = true;
  371. }
  372. }
  373. });
  374. currentBeats.map(function(beat) {
  375. if(beat.repeat) beat.sound.play();
  376. });
  377. if(37 in keysDown) {
  378. player.x -= player.dx * dt;
  379. }
  380. if(39 in keysDown) {
  381. player.x += player.dx * dt;
  382. }
  383. player.x = (player.x + LEVEL_SIZE) % LEVEL_SIZE;
  384. for(var i = entities.length - 1; i >= 0; --i) {
  385. var entity = entities[i];
  386. entity.y += STAGE_SPEED * dt;
  387. entity.onUpdate(dt, gameTime);
  388. if(entity.shouldDelete) {
  389. entities.splice(i, 1);
  390. }
  391. }
  392. meter.onUpdate(dt);
  393. if(gotPoint) {
  394. ++points;
  395. soundManager.play('get');
  396. if(beats.length > 0) {
  397. switchBeat(points % beats.length);
  398. }
  399. }
  400. gameTime += delta;
  401. };
  402. // Render
  403. var translatePoints = function(point) {
  404. var x = point.x - player.x + canvas.width/2;
  405. if(x < -ALLOWANCE) x += LEVEL_SIZE;
  406. if(x > LEVEL_SIZE - ALLOWANCE) x -= LEVEL_SIZE;
  407. return {x:x, y:point.y};
  408. };
  409. var render = function() {
  410. ctx.fillStyle = "black";
  411. ctx.fillRect(0, 0, canvas.width, canvas.height);
  412. entities.map(function(elem) {
  413. elem.draw();
  414. });
  415. drawPlayer(translatePoints(player));
  416. meter.draw();
  417. ctx.fillStyle = "white";
  418. ctx.fillText(points+"", 5, 15);
  419. if(debug) {
  420. drawSpectrum();
  421. }
  422. if(paused) {
  423. ctx.fillStyle = 'white';
  424. ctx.textAlign = 'center';
  425. ctx.fillText("Paused (P to unpause)", canvas.width / 2, canvas.height / 2);
  426. ctx.textAlign = 'start';
  427. }
  428. };
  429. var then;
  430. var main = function() {
  431. var now = Date.now();
  432. var delta = (now - then);
  433. update(delta);
  434. render();
  435. then = now;
  436. };
  437. var start = function() {
  438. reset();
  439. switchBeat(0);
  440. then = Date.now();
  441. setInterval(main, 10);
  442. };
  443. var pause = function() {
  444. paused = !paused;
  445. if(paused) {
  446. currentBeats.map(function(beat) {beat.sound.pause()});
  447. }
  448. else {
  449. currentBeats.map(function(beat) {beat.sound.resume()});
  450. }
  451. };
  452. ctx.textAlign = 'center';
  453. ctx.fillText("Loading...", canvas.width / 2, canvas.height / 2);
  454. ctx.textAlign = 'start';
  455. Q.all([soundPromise]).done(function() {
  456. start();
  457. });
  458. };
  459. var canvas = document.getElementById('game');
  460. var beats = [
  461. {url: 'sound/b1.mp3', duration: 4000},
  462. {url: 'sound/b2.mp3', duration: 4000},
  463. {url: 'sound/b3.mp3', duration: 4000},
  464. {url: 'sound/b4.mp3', duration: 4000},
  465. {url: 'sound/b5.mp3', duration: 4000}
  466. ];
  467. var soundDeferred = Q.defer();
  468. var pSoundManager = soundDeferred.promise;
  469. soundManager.setup({
  470. url: 'swf/',
  471. flashVersion: 9,
  472. onready: function() {
  473. soundDeferred.resolve(this);
  474. }
  475. });
  476. SoundSafari(canvas, beats, pSoundManager);