safari.js 13 KB

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