/bamboojoint.js

https://code.google.com/p/bamboojoint/ · JavaScript · 449 lines · 337 code · 64 blank · 48 comment · 75 complexity · 9e086abd4bdb58ee3cff05b5ae06e7b2 MD5 · raw file

  1. "use strict";
  2. // BambooJoint.Js
  3. // HTML5 Canvas Goban Renderer
  4. //
  5. // Copyright (c) 2011 Stack Exchange, Inc.
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is furnished
  11. // to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22. // SOFTWARE.
  23. //
  24. // -----------------------------------------------------------------------------
  25. //
  26. // Contains lyfe.js, Copyright (c) 2011 Benjamin Dumke-von der Ehe
  27. // Also licensed under the MIT license
  28. // https://bitbucket.org/balpha/lyfe
  29. //
  30. // -----------------------------------------------------------------------------
  31. //
  32. // Usage:
  33. //
  34. // var result = BambooJoint.render(source);
  35. //
  36. // If source is not legal board markup, or if the browser doesn't support the HTML5 canvas, returns null.
  37. // Otherwise, returns an object with a property result.canvas, which is a <canvas> DOM element,
  38. // properties result.width and result.height that give the dimensions of the canvas in pixels,
  39. // and optionally -- if given in the source -- a property result.caption.
  40. //
  41. // -----------------------------------------------------------------------------
  42. window.BambooJoint = (function () {
  43. if (!document.createElement("canvas").getContext)
  44. return { render: function () { return null; }};
  45. function parse(text) {
  46. text = text.replace(/\r/g, "\n");
  47. var lines = Generator(text.split(/\n+/)).filter(function (l) { return /\S/.test(l); }).evaluated(),
  48. result = { board: [] },
  49. board = result.board;
  50. if (lines.any(function (line) { return !/^\$\$/.test(line); })) {
  51. return null;
  52. }
  53. var firstLine = lines.first(),
  54. coordinates;
  55. if (!firstLine)
  56. return null;
  57. result.moveDelta = 0;
  58. // check the first line for options and/or caption
  59. // note that we're not actually replacing (and not storing the result), and that the function gets called at most once
  60. firstLine.replace(/^\$\$(([BW]?)(c?)(\d*)(?:m(\d+))?)(?:\s+(.*)|)$/, function (whole, options, color, coord, size, firstMove, caption) {
  61. caption = (caption || "").replace(/\s+$/, "");
  62. if (!options.length) { // no options -- is this a regular board markup line, or is there a caption?
  63. if (!caption.length) // no caption at all -- we're outta here
  64. return;
  65. if (!/[^\sOW@QPXB#YZCSTM\d?a-z,*+|_-]/.test(caption)) { // all characters are legal markup
  66. if (!/\w{2} \w{2}/.test(caption)) // doesn't seem to be words
  67. return;
  68. }
  69. }
  70. result.whiteFirst = color === "W";
  71. coordinates = coord === "c";
  72. var sizeInt = parseInt(size, 10);
  73. if (isFinite(sizeInt))
  74. result.boardSize = sizeInt;
  75. var firstMoveInt = parseInt(firstMove, 10);
  76. if (isFinite(firstMoveInt))
  77. result.moveDelta = firstMoveInt - 1;
  78. result.caption = caption;
  79. lines = lines.skip(1);
  80. });
  81. var lastRow;
  82. lines.forEach(function (line) {
  83. if (/^\$\$\s(?:[|+-]\s*){2,}$/.test(line)) { // currently, only full horizontal edges are considered
  84. if (lastRow)
  85. lastRow.bottom = true;
  86. if (!board.length || board[board.length - 1].length) {
  87. lastRow = [];
  88. board.push(lastRow);
  89. }
  90. board[board.length - 1].top = true;
  91. return;
  92. }
  93. if (!board.length || board[board.length - 1].length) {
  94. lastRow = [];
  95. board.push(lastRow);
  96. }
  97. var pieces = Generator(function () {
  98. var l = line.length, c;
  99. for (var i = 0; i < l; i++) {
  100. c = line.charAt(i);
  101. if (!/[$\s]/.test(c))
  102. this.yield(c);
  103. }
  104. });
  105. var lastField,
  106. nextIsLeft = false;
  107. pieces.forEach(function (piece) {
  108. if (/[|+-]/.test(piece)) {
  109. if (lastField)
  110. lastField.right = true;
  111. nextIsLeft = true;
  112. return;
  113. }
  114. var field = { piece: piece };
  115. if (nextIsLeft)
  116. field.left = true;
  117. nextIsLeft = false;
  118. lastRow.push(field);
  119. lastField = field;
  120. });
  121. });
  122. // last row is empty
  123. if (board.length && ! board[board.length - 1].length)
  124. board.pop();
  125. var width = 0, height = 0;
  126. if (!board.length) {
  127. //console.log("empty")
  128. return null;
  129. } else {
  130. var diff = false,
  131. width = Generator(board).map(function (r) { return r.length; }).reduce(function (a, b) {
  132. diff = diff || (a !== b);
  133. return Math.max(a, b);
  134. })
  135. height = board.length;
  136. if (diff) {
  137. return null; // the rows don't have equal widths
  138. }
  139. }
  140. if (coordinates) {
  141. var leftEdge = board[0][0].left,
  142. rightEdge = board[0][width - 1].right,
  143. topEdge = board[0].top,
  144. bottomEdge = board[height - 1].bottom;
  145. var boardSize = result.boardSize;
  146. if (!boardSize) {
  147. boardSize = 19;
  148. if (leftEdge && rightEdge)
  149. boardSize = width;
  150. else if (topEdge && bottomEdge)
  151. boardSize = height;
  152. }
  153. if (leftEdge)
  154. result.leftCoordinate = 0;
  155. else if (rightEdge)
  156. result.leftCoordinate = boardSize - width;
  157. else
  158. coordinates = false;
  159. if (topEdge)
  160. result.topCoordinate = boardSize - 1;
  161. else if (bottomEdge)
  162. result.topCoordinate = height - 1;
  163. else
  164. coordinates = false;
  165. if (coordinates)
  166. result.coordinates = true;
  167. }
  168. result.width = width;
  169. result.height = height;
  170. return result;
  171. }
  172. var stoneImages;
  173. function createStoneImages() {
  174. return {
  175. white: createStoneImage("white"),
  176. black: createStoneImage("black"),
  177. both: createStoneImage("both")
  178. };
  179. }
  180. function createStoneImage(color) {
  181. var stone, actualColor, angle1, angle2;
  182. if (color === "both") {
  183. stone = createStoneImage("black");
  184. actualColor = "white";
  185. angle1 = 0.5 * Math.PI;
  186. angle2 = 1.5 * Math.PI;
  187. } else {
  188. stone = document.createElement("canvas");
  189. stone.width = 29;
  190. stone.height = 29;
  191. actualColor = color;
  192. angle1 = 0;
  193. angle2 = 2 * Math.PI;
  194. }
  195. var ctx = stone.getContext("2d");
  196. ctx.save();
  197. ctx.fillStyle = actualColor;
  198. if (color !== "both") {
  199. ctx.shadowOffsetX = 1;
  200. ctx.shadowOffsetY = 1;
  201. ctx.shadowBlur = 3;
  202. ctx.shadowColor = "rgba(0,0,0,.7)";
  203. }
  204. ctx.beginPath();
  205. ctx.arc(14.5, 14.5, 10, angle1, angle2, false);
  206. ctx.fill();
  207. ctx.restore();
  208. // we're doing this in two steps because of a bug in the android browser
  209. // http://code.google.com/p/android/issues/detail?id=21813
  210. ctx.save();
  211. var gradient = ctx.createRadialGradient(14.5, 14.5, 10, 7.5, 7.5, 2);
  212. var c1 = actualColor === "white" ? "#e0e0e0" : "black";
  213. var c2 = actualColor === "black" ? "#404040" : "white";
  214. gradient.addColorStop(0, c1);
  215. gradient.addColorStop(.25, c1);
  216. gradient.addColorStop(1, c2);
  217. ctx.fillStyle = gradient;
  218. ctx.beginPath();
  219. ctx.arc(14.5, 14.5, 10, angle1, angle2, false);
  220. ctx.fill();
  221. ctx.restore();
  222. return stone;
  223. }
  224. function renderParsed(parsed) {
  225. stoneImages = stoneImages || createStoneImages();
  226. var pixWidth = (parsed.width + 1) * 22,
  227. pixHeight = (parsed.height + 1) * 22,
  228. bgColor = '#d3823b';
  229. if (parsed.coordinates) {
  230. pixWidth += 6;
  231. pixHeight += 6;
  232. }
  233. var canvas = document.createElement("canvas");
  234. canvas.width = pixWidth;
  235. canvas.height = pixHeight;
  236. var ctx = canvas.getContext("2d");
  237. ctx.fillStyle = bgColor;
  238. ctx.fillRect(0, 0, pixWidth, pixHeight);
  239. function stone(x, y, color) {
  240. ctx.drawImage(stoneImages[color], x - 14.5, y - 14.5)
  241. }
  242. function putlines(x, y, top, right, bottom, left) {
  243. ctx.beginPath();
  244. ctx.moveTo(x - (left ? 0 : 11), y);
  245. ctx.lineTo(x + (right ? 0 : 11), y);
  246. ctx.moveTo(x, y - (top ? 0 : 11));
  247. ctx.lineTo(x, y + (bottom ? 0 : 11));
  248. ctx.stroke();
  249. }
  250. function mark_circle(x, y) {
  251. ctx.save();
  252. ctx.lineWidth = 2;
  253. ctx.strokeStyle = "red";
  254. ctx.beginPath();
  255. ctx.arc(x, y, 5, 0, 2 * Math.PI, false);
  256. ctx.stroke();
  257. ctx.restore();
  258. }
  259. function mark_square(x, y) {
  260. ctx.save();
  261. ctx.fillStyle = "red";
  262. ctx.fillRect(x - 5, y - 5, 10, 10);
  263. ctx.restore();
  264. }
  265. function mark_triangle(x, y) {
  266. ctx.save();
  267. ctx.fillStyle = "red";
  268. ctx.beginPath();
  269. ctx.moveTo(x, y - 6);
  270. ctx.lineTo(x + 6, y + 4);
  271. ctx.lineTo(x - 6, y + 4);
  272. ctx.closePath();
  273. ctx.fill();
  274. ctx.restore();
  275. }
  276. function mark_x(x, y) {
  277. ctx.save();
  278. ctx.strokeStyle = "red";
  279. ctx.lineWidth = 2;
  280. ctx.beginPath();
  281. ctx.moveTo(x - 5, y - 5);
  282. ctx.lineTo(x + 5, y + 5);
  283. ctx.moveTo(x + 5, y - 5);
  284. ctx.lineTo(x - 5, y + 5);
  285. ctx.stroke();
  286. ctx.restore();
  287. }
  288. var edge = parsed.coordinates ? 28.5 : 22.5;
  289. Generator(parsed.board).forEach(function (line, row) {
  290. Generator(line).forEach(function (field, col) {
  291. var x = col * 22 + edge,
  292. y = row * 22 + edge,
  293. piece = field.piece;
  294. if (piece !== "_")
  295. putlines(x, y, line.top, field.right, line.bottom, field.left);
  296. ctx.save();
  297. if (/[OW@QP]/.test(piece))
  298. stone(x, y, "white");
  299. else if (/[XB#YZ]/.test(piece))
  300. stone(x, y, "black");
  301. if (/[BWC]/.test(piece))
  302. mark_circle(x, y);
  303. else if (/[#@S]/.test(piece))
  304. mark_square(x, y);
  305. else if (/[YQT]/.test(piece))
  306. mark_triangle(x, y);
  307. else if (/[ZPM]/.test(piece))
  308. mark_x(x, y);
  309. else if (piece === "*")
  310. stone(x, y, "both");
  311. else if (/^\d$/.test(piece)) {
  312. var val = parseInt(piece, 10);
  313. if (val === 0)
  314. val = 10;
  315. var isBlack = (val % 2 === 1) ^ parsed.whiteFirst;
  316. stone(x, y, isBlack ? "black" : "white");
  317. ctx.font = "12px sans-serif";
  318. ctx.textAlign = "center";
  319. ctx.fillStyle = isBlack ? "white" : "black";
  320. ctx.fillText(val + parsed.moveDelta, x, y + 4);
  321. } else if (piece === "?") {
  322. ctx.fillStyle = "rgba(255,255,255,0.5)";
  323. // using integer coordinates here to avoid seeing very thin lines between adjacent shaded points
  324. ctx.fillRect(x - 11.5, y - 11.5, 22, 22);
  325. } else if (/^[a-z]$/.test(piece)) {
  326. ctx.font = "bold 15px sans-serif";
  327. ctx.textAlign = "center";
  328. ctx.fillStyle = "black";
  329. ctx.lineWidth = 6;
  330. ctx.strokeStyle = bgColor;
  331. ctx.strokeText(piece, x, y + 5);
  332. ctx.fillText(piece, x, y + 5);
  333. ctx.lineWidth = 1;
  334. } else if (piece === ",") {
  335. ctx.fillStyle = "black";
  336. ctx.beginPath();
  337. ctx.arc(x, y, 2.5, 0, 2 * Math.PI, false);
  338. ctx.fill();
  339. }
  340. ctx.restore();
  341. });
  342. });
  343. if (parsed.coordinates) {
  344. ctx.save();
  345. ctx.fillStyle = "#6b421e";
  346. ctx.font = "10px sans-serif";
  347. for (var row = 0; row < parsed.height; row++) {
  348. ctx.textAlign = "right";
  349. ctx.fillText(parsed.topCoordinate - row + 1, 16, row * 22 + 31.5);
  350. }
  351. for (var col = 0; col < parsed.width; col++) {
  352. ctx.textAlign = "center";
  353. var letter = parsed.leftCoordinate + col + 1;
  354. if (letter >= 9) // skip "I"
  355. letter++;
  356. ctx.fillText(String.fromCharCode(64 + letter), col * 22 + 28.5, 12);
  357. }
  358. ctx.restore();
  359. }
  360. return {
  361. canvas: canvas,
  362. width: pixWidth,
  363. height: pixHeight
  364. };
  365. }
  366. return {
  367. render: function (source) {
  368. var parsed = parse(source);
  369. if (!parsed)
  370. return null;
  371. var result = renderParsed(parsed);
  372. if (parsed.caption && parsed.caption.length)
  373. result.caption = parsed.caption;
  374. return result;
  375. }
  376. }
  377. })();
  378. // lyfe.js
  379. (function(){var k;k=Array.prototype.indexOf?function(a,b){return a.indexOf(b)}:function(a,b){for(var c=a.length,d=0;d<c;d++)if(d in a&&a[d]===b)return d;return-1};var h={},e=function(a){if(!(this instanceof e))return new e(a);this.forEach=typeof a==="function"?i(a):a.constructor===Array?p(a):q(a)},l=function(){throw h;},j=function(a){this.message=a;this.name="IterationError"};j.prototype=Error.prototype;var i=function(a){return function(b,c){var d=!1,f=0,m={yield:function(a){if(d)throw new j("yield after end of iteration");
  380. a=b.call(c,a,f,l);f++;return a},yieldMany:function(a){(a instanceof e?a:new e(a)).forEach(function(a){m.yield(a)})},stop:l};try{a.call(m)}catch(g){if(g!==h)throw g;}finally{d=!0}}},p=function(a){return i(function(){for(var b=a.length,c=0;c<b;c++)c in a&&this.yield(a[c])})},q=function(a){return i(function(){for(var b in a)a.hasOwnProperty(b)&&this.yield([b,a[b]])})};e.prototype={toArray:function(){var a=[];this.forEach(function(b){a.push(b)});return a},filter:function(a,b){var c=this;return new e(function(){var d=
  381. this;c.forEach(function(c){a.call(b,c)&&d.yield(c)})})},take:function(a){var b=this;return new e(function(){var c=this;b.forEach(function(b,f){f>=a&&c.stop();c.yield(b)})})},skip:function(a){var b=this;return new e(function(){var c=this;b.forEach(function(b,f){f>=a&&c.yield(b)})})},map:function(a,b){var c=this;return new e(function(){var d=this;c.forEach(function(c){d.yield(a.call(b,c))})})},zipWithArray:function(a,b){typeof b==="undefined"&&(b=function(a,b){return[a,b]});var c=this;return new e(function(){var d=
  382. a.length,f=this;c.forEach(function(c,e){e>=d&&f.stop();f.yield(b(c,a[e]))})})},reduce:function(a,b){var c,d;arguments.length<2?c=!0:(c=!1,d=b);this.forEach(function(b){c?(d=b,c=!1):d=a(d,b)});return d},and:function(a){var b=this;return new e(function(){this.yieldMany(b);this.yieldMany(a)})},takeWhile:function(a){var b=this;return new e(function(){var c=this;b.forEach(function(b){a(b)?c.yield(b):c.stop()})})},skipWhile:function(a){var b=this;return new e(function(){var c=this,d=!0;b.forEach(function(b){(d=
  383. d&&a(b))||c.yield(b)})})},all:function(a){var b=!0;this.forEach(function(c,d,e){if(!(a?a(c):c))b=!1,e()});return b},any:function(a){var b=!1;this.forEach(function(c,d,e){if(a?a(c):c)b=!0,e()});return b},first:function(){var a;this.forEach(function(b,c,d){a=b;d()});return a},groupBy:function(a){var b=this;return new e(function(){var c=[],d=[];b.forEach(function(b){var e=a(b),g=k(c,e);g===-1?(c.push(e),d.push([b])):d[g].push(b)});this.yieldMany((new e(c)).zipWithArray(d,function(a,b){var c=new e(b);
  384. c.key=a;return c}))})},evaluated:function(){return new e(this.toArray())},except:function(a){return this.filter(function(b){return b!==a})},sortBy:function(a){var b=this;return new e(function(){var c=b.toArray(),d=n(0,c.length).toArray(),f=this;d.sort(function(b,e){var d=a(c[b]),f=a(c[e]);if(typeof d===typeof f){if(d===f)return b<e?-1:1;if(d<f)return-1;if(d>f)return 1}throw new TypeError("cannot compare "+d+" and "+f);});(new e(d)).forEach(function(a){f.yield(c[a])})})}};var o=function(a,b){var c=
  385. a;typeof b==="undefined"&&(b=1);return new e(function(){for(;;)this.yield(c),c+=b})},n=function(a,b){return o(a,1).take(b)};window.Generator=e;e.BreakIteration=h;e.Count=o;e.Range=n;e.IterationError=j})();