safari.js 14 KB

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