fish.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. var RENDERER = {
  2. POINT_INTERVAL: 5,
  3. FISH_COUNT: 3,
  4. MAX_INTERVAL_COUNT: 50,
  5. INIT_HEIGHT_RATE: 0.5,
  6. THRESHOLD: 50,
  7. init: function () {
  8. this.setParameters();
  9. this.reconstructMethods();
  10. this.setup();
  11. this.bindEvent();
  12. this.render();
  13. },
  14. setParameters: function () {
  15. this.$window = $(window);
  16. this.$container = $('#jsi-flying-fish-container');
  17. this.$canvas = $('<canvas />');
  18. this.context = this.$canvas.appendTo(this.$container).get(0).getContext('2d');
  19. this.points = [];
  20. this.fishes = [];
  21. this.watchIds = [];
  22. },
  23. createSurfacePoints: function () {
  24. var count = Math.round(this.width / this.POINT_INTERVAL);
  25. this.pointInterval = this.width / (count - 1);
  26. this.points.push(new SURFACE_POINT(this, 0));
  27. for (var i = 1; i < count; i++) {
  28. var point = new SURFACE_POINT(this, i * this.pointInterval),
  29. previous = this.points[i - 1];
  30. point.setPreviousPoint(previous);
  31. previous.setNextPoint(point);
  32. this.points.push(point);
  33. }
  34. },
  35. reconstructMethods: function () {
  36. this.watchWindowSize = this.watchWindowSize.bind(this);
  37. this.jdugeToStopResize = this.jdugeToStopResize.bind(this);
  38. this.startEpicenter = this.startEpicenter.bind(this);
  39. this.moveEpicenter = this.moveEpicenter.bind(this);
  40. this.reverseVertical = this.reverseVertical.bind(this);
  41. this.render = this.render.bind(this);
  42. },
  43. setup: function () {
  44. this.points.length = 0;
  45. this.fishes.length = 0;
  46. this.watchIds.length = 0;
  47. this.intervalCount = this.MAX_INTERVAL_COUNT;
  48. this.width = this.$container.width();
  49. this.height = this.$container.height();
  50. this.fishCount = this.FISH_COUNT * this.width / 500 * this.height / 500;
  51. this.$canvas.attr({width: this.width, height: this.height});
  52. this.reverse = false;
  53. this.fishes.push(new FISH(this));
  54. this.createSurfacePoints();
  55. },
  56. watchWindowSize: function () {
  57. this.clearTimer();
  58. this.tmpWidth = this.$window.width();
  59. this.tmpHeight = this.$window.height();
  60. this.watchIds.push(setTimeout(this.jdugeToStopResize, this.WATCH_INTERVAL));
  61. },
  62. clearTimer: function () {
  63. while (this.watchIds.length > 0) {
  64. clearTimeout(this.watchIds.pop());
  65. }
  66. },
  67. jdugeToStopResize: function () {
  68. var width = this.$window.width(),
  69. height = this.$window.height(),
  70. stopped = (width == this.tmpWidth && height == this.tmpHeight);
  71. this.tmpWidth = width;
  72. this.tmpHeight = height;
  73. if (stopped) {
  74. this.setup();
  75. }
  76. },
  77. bindEvent: function () {
  78. this.$window.on('resize', this.watchWindowSize);
  79. this.$container.on('mouseenter', this.startEpicenter);
  80. this.$container.on('mousemove', this.moveEpicenter);
  81. this.$container.on('click', this.reverseVertical);
  82. },
  83. getAxis: function (event) {
  84. var offset = this.$container.offset();
  85. return {
  86. x: event.clientX - offset.left + this.$window.scrollLeft(),
  87. y: event.clientY - offset.top + this.$window.scrollTop()
  88. };
  89. },
  90. startEpicenter: function (event) {
  91. this.axis = this.getAxis(event);
  92. },
  93. moveEpicenter: function (event) {
  94. var axis = this.getAxis(event);
  95. if (!this.axis) {
  96. this.axis = axis;
  97. }
  98. this.generateEpicenter(axis.x, axis.y, axis.y - this.axis.y);
  99. this.axis = axis;
  100. },
  101. generateEpicenter: function (x, y, velocity) {
  102. if (y < this.height / 2 - this.THRESHOLD || y > this.height / 2 + this.THRESHOLD) {
  103. return;
  104. }
  105. var index = Math.round(x / this.pointInterval);
  106. if (index < 0 || index >= this.points.length) {
  107. return;
  108. }
  109. this.points[index].interfere(y, velocity);
  110. },
  111. reverseVertical: function () {
  112. this.reverse = !this.reverse;
  113. for (var i = 0, count = this.fishes.length; i < count; i++) {
  114. this.fishes[i].reverseVertical();
  115. }
  116. },
  117. controlStatus: function () {
  118. for (var i = 0, count = this.points.length; i < count; i++) {
  119. this.points[i].updateSelf();
  120. }
  121. for (var i = 0, count = this.points.length; i < count; i++) {
  122. this.points[i].updateNeighbors();
  123. }
  124. if (this.fishes.length < this.fishCount) {
  125. if (--this.intervalCount == 0) {
  126. this.intervalCount = this.MAX_INTERVAL_COUNT;
  127. this.fishes.push(new FISH(this));
  128. }
  129. }
  130. },
  131. render: function () {
  132. requestAnimationFrame(this.render);
  133. this.controlStatus();
  134. this.context.clearRect(0, 0, this.width, this.height);
  135. this.context.fillStyle = 'hsl(0, 0%, 95%)';
  136. for (var i = 0, count = this.fishes.length; i < count; i++) {
  137. this.fishes[i].render(this.context);
  138. }
  139. this.context.save();
  140. this.context.globalCompositeOperation = 'xor';
  141. this.context.beginPath();
  142. this.context.moveTo(0, this.reverse ? 0 : this.height);
  143. for (var i = 0, count = this.points.length; i < count; i++) {
  144. this.points[i].render(this.context);
  145. }
  146. this.context.lineTo(this.width, this.reverse ? 0 : this.height);
  147. this.context.closePath();
  148. this.context.fill();
  149. this.context.restore();
  150. }
  151. };
  152. var SURFACE_POINT = function (renderer, x) {
  153. this.renderer = renderer;
  154. this.x = x;
  155. this.init();
  156. };
  157. SURFACE_POINT.prototype = {
  158. SPRING_CONSTANT: 0.03,
  159. SPRING_FRICTION: 0.9,
  160. WAVE_SPREAD: 0.3,
  161. ACCELARATION_RATE: 0.01,
  162. init: function () {
  163. this.initHeight = this.renderer.height * this.renderer.INIT_HEIGHT_RATE;
  164. this.height = this.initHeight;
  165. this.fy = 0;
  166. this.force = {previous: 0, next: 0};
  167. },
  168. setPreviousPoint: function (previous) {
  169. this.previous = previous;
  170. },
  171. setNextPoint: function (next) {
  172. this.next = next;
  173. },
  174. interfere: function (y, velocity) {
  175. this.fy = this.renderer.height * this.ACCELARATION_RATE * ((this.renderer.height - this.height - y) >= 0 ? -1 : 1) * Math.abs(velocity);
  176. },
  177. updateSelf: function () {
  178. this.fy += this.SPRING_CONSTANT * (this.initHeight - this.height);
  179. this.fy *= this.SPRING_FRICTION;
  180. this.height += this.fy;
  181. },
  182. updateNeighbors: function () {
  183. if (this.previous) {
  184. this.force.previous = this.WAVE_SPREAD * (this.height - this.previous.height);
  185. }
  186. if (this.next) {
  187. this.force.next = this.WAVE_SPREAD * (this.height - this.next.height);
  188. }
  189. },
  190. render: function (context) {
  191. if (this.previous) {
  192. this.previous.height += this.force.previous;
  193. this.previous.fy += this.force.previous;
  194. }
  195. if (this.next) {
  196. this.next.height += this.force.next;
  197. this.next.fy += this.force.next;
  198. }
  199. context.lineTo(this.x, this.renderer.height - this.height);
  200. }
  201. };
  202. var FISH = function (renderer) {
  203. this.renderer = renderer;
  204. this.init();
  205. };
  206. FISH.prototype = {
  207. GRAVITY: 0.4,
  208. init: function () {
  209. this.direction = Math.random() < 0.5;
  210. this.x = this.direction ? (this.renderer.width + this.renderer.THRESHOLD) : -this.renderer.THRESHOLD;
  211. this.previousY = this.y;
  212. this.vx = this.getRandomValue(4, 10) * (this.direction ? -1 : 1);
  213. if (this.renderer.reverse) {
  214. this.y = this.getRandomValue(this.renderer.height * 1 / 10, this.renderer.height * 4 / 10);
  215. this.vy = this.getRandomValue(2, 5);
  216. this.ay = this.getRandomValue(0.05, 0.2);
  217. } else {
  218. this.y = this.getRandomValue(this.renderer.height * 6 / 10, this.renderer.height * 9 / 10);
  219. this.vy = this.getRandomValue(-5, -2);
  220. this.ay = this.getRandomValue(-0.2, -0.05);
  221. }
  222. this.isOut = false;
  223. this.theta = 0;
  224. this.phi = 0;
  225. },
  226. getRandomValue: function (min, max) {
  227. return min + (max - min) * Math.random();
  228. },
  229. reverseVertical: function () {
  230. this.isOut = !this.isOut;
  231. this.ay *= -1;
  232. },
  233. controlStatus: function (context) {
  234. this.previousY = this.y;
  235. this.x += this.vx;
  236. this.y += this.vy;
  237. this.vy += this.ay;
  238. if (this.renderer.reverse) {
  239. if (this.y > this.renderer.height * this.renderer.INIT_HEIGHT_RATE) {
  240. this.vy -= this.GRAVITY;
  241. this.isOut = true;
  242. } else {
  243. if (this.isOut) {
  244. this.ay = this.getRandomValue(0.05, 0.2);
  245. }
  246. this.isOut = false;
  247. }
  248. } else {
  249. if (this.y < this.renderer.height * this.renderer.INIT_HEIGHT_RATE) {
  250. this.vy += this.GRAVITY;
  251. this.isOut = true;
  252. } else {
  253. if (this.isOut) {
  254. this.ay = this.getRandomValue(-0.2, -0.05);
  255. }
  256. this.isOut = false;
  257. }
  258. }
  259. if (!this.isOut) {
  260. this.theta += Math.PI / 20;
  261. this.theta %= Math.PI * 2;
  262. this.phi += Math.PI / 30;
  263. this.phi %= Math.PI * 2;
  264. }
  265. this.renderer.generateEpicenter(this.x + (this.direction ? -1 : 1) * this.renderer.THRESHOLD, this.y, this.y - this.previousY);
  266. if (this.vx > 0 && this.x > this.renderer.width + this.renderer.THRESHOLD || this.vx < 0 && this.x < -this.renderer.THRESHOLD) {
  267. this.init();
  268. }
  269. },
  270. render: function (context) {
  271. context.save();
  272. context.translate(this.x, this.y);
  273. context.rotate(Math.PI + Math.atan2(this.vy, this.vx));
  274. context.scale(1, this.direction ? 1 : -1);
  275. context.beginPath();
  276. context.moveTo(-30, 0);
  277. context.bezierCurveTo(-20, 15, 15, 10, 40, 0);
  278. context.bezierCurveTo(15, -10, -20, -15, -30, 0);
  279. context.fill();
  280. context.save();
  281. context.translate(40, 0);
  282. context.scale(0.9 + 0.2 * Math.sin(this.theta), 1);
  283. context.beginPath();
  284. context.moveTo(0, 0);
  285. context.quadraticCurveTo(5, 10, 20, 8);
  286. context.quadraticCurveTo(12, 5, 10, 0);
  287. context.quadraticCurveTo(12, -5, 20, -8);
  288. context.quadraticCurveTo(5, -10, 0, 0);
  289. context.fill();
  290. context.restore();
  291. context.save();
  292. context.translate(-3, 0);
  293. context.rotate((Math.PI / 3 + Math.PI / 10 * Math.sin(this.phi)) * (this.renderer.reverse ? -1 : 1));
  294. context.beginPath();
  295. if (this.renderer.reverse) {
  296. context.moveTo(5, 0);
  297. context.bezierCurveTo(10, 10, 10, 30, 0, 40);
  298. context.bezierCurveTo(-12, 25, -8, 10, 0, 0);
  299. } else {
  300. context.moveTo(-5, 0);
  301. context.bezierCurveTo(-10, -10, -10, -30, 0, -40);
  302. context.bezierCurveTo(12, -25, 8, -10, 0, 0);
  303. }
  304. context.closePath();
  305. context.fill();
  306. context.restore();
  307. context.restore();
  308. this.controlStatus(context);
  309. }
  310. };
  311. $(function () {
  312. RENDERER.init();
  313. });