not really known
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

785 lines
22 KiB

  1. // Copyright (c) 2014,2015 Walter Bender
  2. //
  3. // This program is free software; you can redistribute it and/or
  4. // modify it under the terms of the The GNU Affero General Public
  5. // License as published by the Free Software Foundation; either
  6. // version 3 of the License, or (at your option) any later version.
  7. //
  8. // You should have received a copy of the GNU Affero General Public
  9. // License along with this library; if not, write to the Free Software
  10. // Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA
  11. function format(str, data) {
  12. str = str.replace(/{([a-zA-Z0-9.]*)}/g,
  13. function (match, name) {
  14. x = data;
  15. name.split('.').forEach(function (v) {
  16. if (x === undefined) {
  17. console.log('Undefined value in template string', str, name, x, v);
  18. }
  19. x = x[v];
  20. });
  21. return x;
  22. });
  23. return str.replace(/{_([a-zA-Z0-9]+)}/g,
  24. function (match, item) {
  25. return _(item);
  26. });
  27. };
  28. function canvasPixelRatio() {
  29. var devicePixelRatio = window.devicePixelRatio || 1;
  30. var context = document.querySelector('#myCanvas').getContext('2d');
  31. var backingStoreRatio = context.webkitBackingStorePixelRatio ||
  32. context.mozBackingStorePixelRatio ||
  33. context.msBackingStorePixelRatio ||
  34. context.oBackingStorePixelRatio ||
  35. context.backingStorePixelRatio || 1;
  36. return devicePixelRatio / backingStoreRatio;
  37. };
  38. function windowHeight() {
  39. var onAndroid = /Android/i.test(navigator.userAgent);
  40. if (onAndroid) {
  41. return window.outerHeight;
  42. } else {
  43. return window.innerHeight;
  44. }
  45. };
  46. function windowWidth() {
  47. var onAndroid = /Android/i.test(navigator.userAgent);
  48. if (onAndroid) {
  49. return window.outerWidth;
  50. } else {
  51. return window.innerWidth;
  52. }
  53. };
  54. function httpGet(projectName) {
  55. var xmlHttp = null;
  56. xmlHttp = new XMLHttpRequest();
  57. if (projectName === null) {
  58. xmlHttp.open("GET", window.server, false);
  59. xmlHttp.setRequestHeader('x-api-key', '3tgTzMXbbw6xEKX7');
  60. } else {
  61. xmlHttp.open("GET", window.server + projectName, false);
  62. xmlHttp.setRequestHeader('x-api-key', '3tgTzMXbbw6xEKX7');
  63. }
  64. xmlHttp.send();
  65. if (xmlHttp.status > 299) {
  66. throw 'Error from server';
  67. }
  68. return xmlHttp.responseText;
  69. };
  70. function httpPost(projectName, data) {
  71. var xmlHttp = null;
  72. xmlHttp = new XMLHttpRequest();
  73. xmlHttp.open("POST", window.server + projectName, false);
  74. xmlHttp.setRequestHeader('x-api-key', '3tgTzMXbbw6xEKX7');
  75. xmlHttp.send(data);
  76. // return xmlHttp.responseText;
  77. return 'https://apps.facebook.com/turtleblocks/?file=' + projectName;
  78. };
  79. function HttpRequest(url, loadCallback, userCallback) {
  80. // userCallback is an optional callback-handler.
  81. var req = this.request = new XMLHttpRequest();
  82. this.handler = loadCallback;
  83. this.url = url;
  84. this.localmode = Boolean(self.location.href.search(/^file:/i) === 0);
  85. this.userCallback = userCallback;
  86. var objref = this;
  87. try {
  88. req.open('GET', url);
  89. req.onreadystatechange = function() { objref.handler(); };
  90. req.send('');
  91. }
  92. catch(e) {
  93. if (self.console) console.log('Failed to load resource from ' + url + ': Network error.');
  94. if (typeof userCallback === 'function') userCallback(false, 'network error');
  95. this.request = this.handler = this.userCallback = null;
  96. }
  97. };
  98. function docByTagName(tag) {
  99. document.getElementsByTagName(tag);
  100. };
  101. function docById(id) {
  102. return document.getElementById(id);
  103. };
  104. function last(myList) {
  105. var i = myList.length;
  106. if (i === 0) {
  107. return null;
  108. } else {
  109. return myList[i - 1];
  110. }
  111. };
  112. function doSVG(canvas, logo, turtles, width, height, scale) {
  113. // Aggregate SVG output from each turtle. If there is none, use
  114. // the MUSICICON.
  115. var MUSICICON = '<g transform="matrix(20,0,0,20,-2500,-200)"> <g style="font-size:20px;font-family:Sans;text-anchor:end;fill:#000000" transform="translate(0.32906,-0.2)"> <path d="m 138.47094,26.82 q 0,-1.16 1.24,-2.02 0.96,-0.64 1.94,-0.64 0.68,0.02 1.18,0.34 l 0,-11.84 0.44,0 0,12.94 q 0,1.32 -1.34,2.1 -0.86,0.5 -1.8,0.5 -0.98,0 -1.44,-0.7 -0.22,-0.32 -0.22,-0.68 z" /> </g> <g transform="translate(-12.52094,4.8)" style="font-size:20px;font-family:Sans;text-anchor:end;fill:#000000"> <path d="m 138.47094,26.82 q 0,-1.16 1.24,-2.02 0.96,-0.64 1.94,-0.64 0.68,0.02 1.18,0.34 l 0,-11.84 0.44,0 0,12.94 q 0,1.32 -1.34,2.1 -0.86,0.5 -1.8,0.5 -0.98,0 -1.44,-0.7 -0.22,-0.32 -0.22,-0.68 z" /> </g> <path style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 130.81346,17 12.29007,-5 0,2 -12.29007,5 z" /> </g>';
  116. var turtleSVG = '';
  117. for (var turtle in turtles.turtleList) {
  118. turtles.turtleList[turtle].closeSVG();
  119. turtleSVG += turtles.turtleList[turtle].svgOutput;
  120. }
  121. var svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '">\n';
  122. svg += '<g transform="scale(' + scale + ',' + scale + ')">\n';
  123. svg += logo.svgOutput;
  124. svg += '</g>';
  125. if (turtleSVG === '') {
  126. svg += MUSICICON;
  127. } else {
  128. svg += turtleSVG;
  129. }
  130. svg += '</svg>';
  131. return svg;
  132. };
  133. function isSVGEmpty(turtles) {
  134. for (var turtle in turtles.turtleList) {
  135. turtles.turtleList[turtle].closeSVG();
  136. if (turtles.turtleList[turtle].svgOutput !== '') {
  137. return false;
  138. }
  139. }
  140. return true;
  141. };
  142. function fileExt(file) {
  143. var parts = file.split('.');
  144. if (parts.length === 1 || (parts[0] === '' && parts.length === 2)) {
  145. return '';
  146. }
  147. return parts.pop();
  148. };
  149. function fileBasename(file) {
  150. var parts = file.split('.');
  151. if (parts.length === 1) {
  152. return parts[0];
  153. } else if (parts[0] === '' && parts.length === 2) {
  154. return file;
  155. } else {
  156. parts.pop(); // throw away suffix
  157. return parts.join('.');
  158. }
  159. };
  160. // Needed to generate new data for localization.ini
  161. // var translated = "";
  162. function _(text) {
  163. replaced = text;
  164. replace = [",", "(", ")", "?", "¿", "<", ">", ".", '"\n', '"', ":", "%s", "%d", "/", "'", ";", "×", "!", "¡"];
  165. for (var p = 0; p < replace.length; p++) {
  166. replaced = replaced.replace(replace[p], "");
  167. }
  168. replaced = replaced.replace(/ /g, '-');
  169. // Needed to generate new data for localization.ini
  170. // txt = "\n" + replaced + " = " + text;
  171. // if (translated.lastIndexOf(txt) === -1) {
  172. // translated = translated + txt;
  173. // }
  174. // You can log translated in console.log(translated)
  175. try {
  176. translation = document.webL10n.get(replaced);
  177. if (translation === '') {
  178. translation = text;
  179. };
  180. return translation;
  181. } catch (e) {
  182. console.log('i18n error: ' + text);
  183. return text;
  184. }
  185. };
  186. function toTitleCase(str) {
  187. if (typeof str !== 'string')
  188. return;
  189. var tempStr = '';
  190. if (str.length > 1)
  191. tempStr = str.substring(1);
  192. return str.toUpperCase()[0] + tempStr;
  193. }
  194. function processRawPluginData(rawData, palettes, blocks, errorMsg, evalFlowDict, evalArgDict, evalParameterDict, evalSetterDict, evalOnStartList, evalOnStopList) {
  195. // console.log(rawData);
  196. var lineData = rawData.split('\n');
  197. var cleanData = '';
  198. // We need to remove blank lines and comments and then
  199. // join the data back together for processing as JSON.
  200. for (var i = 0; i < lineData.length; i++) {
  201. if (lineData[i].length === 0) {
  202. continue;
  203. }
  204. if (lineData[i][0] === '/') {
  205. continue;
  206. }
  207. cleanData += lineData[i];
  208. }
  209. // Note to plugin developers: You may want to comment out this
  210. // try/catch while debugging your plugin.
  211. try {
  212. var obj = processPluginData(cleanData.replace(/\n/g,''), palettes, blocks, evalFlowDict, evalArgDict, evalParameterDict, evalSetterDict, evalOnStartList, evalOnStopList);
  213. } catch (e) {
  214. var obj = null;
  215. errorMsg('Error loading plugin: ' + e);
  216. }
  217. return obj;
  218. };
  219. function processPluginData(pluginData, palettes, blocks, evalFlowDict, evalArgDict, evalParameterDict, evalSetterDict, evalOnStartList, evalOnStopList) {
  220. // Plugins are JSON-encoded dictionaries.
  221. // console.log(pluginData);
  222. var obj = JSON.parse(pluginData);
  223. // Create a palette entry.
  224. var newPalette = false;
  225. if ('PALETTEPLUGINS' in obj) {
  226. for (var name in obj['PALETTEPLUGINS']) {
  227. PALETTEICONS[name] = obj['PALETTEPLUGINS'][name];
  228. var fillColor = '#ff0066';
  229. if ('PALETTEFILLCOLORS' in obj) {
  230. if (name in obj['PALETTEFILLCOLORS']) {
  231. var fillColor = obj['PALETTEFILLCOLORS'][name];
  232. // console.log(fillColor);
  233. }
  234. }
  235. PALETTEFILLCOLORS[name] = fillColor;
  236. var strokeColor = '#ef003e';
  237. if ('PALETTESTROKECOLORS' in obj) {
  238. if (name in obj['PALETTESTROKECOLORS']) {
  239. var strokeColor = obj['PALETTESTROKECOLORS'][name];
  240. // console.log(strokeColor);
  241. }
  242. }
  243. PALETTESTROKECOLORS[name] = strokeColor;
  244. var highlightColor = '#ffb1b3';
  245. if ('PALETTEHIGHLIGHTCOLORS' in obj) {
  246. if (name in obj['PALETTEHIGHLIGHTCOLORS']) {
  247. var highlightColor = obj['PALETTEHIGHLIGHTCOLORS'][name];
  248. // console.log(highlightColor);
  249. }
  250. }
  251. PALETTEHIGHLIGHTCOLORS[name] = highlightColor;
  252. var strokeHighlightColor = '#404040';
  253. if ('HIGHLIGHTSTROKECOLORS' in obj) {
  254. if (name in obj['HIGHLIGHTSTROKECOLORS']) {
  255. var strokeHighlightColor = obj['HIGHLIGHTSTROKECOLORS'][name];
  256. // console.log(highlightColor);
  257. }
  258. }
  259. HIGHLIGHTSTROKECOLORS[name] = strokeHighlightColor;
  260. if (name in palettes.buttons) {
  261. console.log('palette ' + name + ' already exists');
  262. } else {
  263. console.log('adding palette ' + name);
  264. palettes.add(name);
  265. newPalette = true;
  266. }
  267. }
  268. }
  269. if (newPalette) {
  270. try {
  271. palettes.makePalettes();
  272. } catch (e) {
  273. console.log('makePalettes: ' + e);
  274. }
  275. }
  276. // Define the image blocks
  277. if ('IMAGES' in obj) {
  278. for (var blkName in obj['IMAGES']) {
  279. pluginsImages[blkName] = obj['IMAGES'][blkName];
  280. }
  281. }
  282. // Populate the flow-block dictionary, i.e., the code that is
  283. // eval'd by this block.
  284. if ('FLOWPLUGINS' in obj) {
  285. for (var flow in obj['FLOWPLUGINS']) {
  286. evalFlowDict[flow] = obj['FLOWPLUGINS'][flow];
  287. }
  288. }
  289. // Populate the arg-block dictionary, i.e., the code that is
  290. // eval'd by this block.
  291. if ('ARGPLUGINS' in obj) {
  292. for (var arg in obj['ARGPLUGINS']) {
  293. evalArgDict[arg] = obj['ARGPLUGINS'][arg];
  294. }
  295. }
  296. // Populate the setter dictionary, i.e., the code that is
  297. // used to set a value block.
  298. if ('SETTERPLUGINS' in obj) {
  299. for (var setter in obj['SETTERPLUGINS']) {
  300. evalSetterDict[setter] = obj['SETTERPLUGINS'][setter];
  301. }
  302. }
  303. // Create the plugin protoblocks.
  304. if ('BLOCKPLUGINS' in obj) {
  305. for (var block in obj['BLOCKPLUGINS']) {
  306. console.log('adding plugin block ' + block);
  307. try {
  308. eval(obj['BLOCKPLUGINS'][block]);
  309. } catch (e) {
  310. console.log('Failed to load plugin for ' + block + ': ' + e);
  311. }
  312. }
  313. }
  314. // Create the globals.
  315. if ('GLOBALS' in obj) {
  316. eval(obj['GLOBALS']);
  317. }
  318. if ('PARAMETERPLUGINS' in obj) {
  319. for (var parameter in obj['PARAMETERPLUGINS']) {
  320. evalParameterDict[parameter] = obj['PARAMETERPLUGINS'][parameter];
  321. }
  322. }
  323. // Code to execute when plugin is loaded
  324. if ('ONLOAD' in obj) {
  325. for (var arg in obj['ONLOAD']) {
  326. eval(obj['ONLOAD'][arg]);
  327. }
  328. }
  329. // Code to execute when turtle code is started
  330. if ('ONSTART' in obj) {
  331. for (var arg in obj['ONSTART']) {
  332. evalOnStartList[arg] = obj['ONSTART'][arg];
  333. }
  334. }
  335. // Code to execute when turtle code is stopped
  336. if ('ONSTOP' in obj) {
  337. for (var arg in obj['ONSTOP']) {
  338. evalOnStopList[arg] = obj['ONSTOP'][arg];
  339. }
  340. }
  341. // Push the protoblocks onto their palettes.
  342. for (var protoblock in blocks.protoBlockDict) {
  343. if (blocks.protoBlockDict[protoblock].palette === undefined) {
  344. console.log('Cannot find palette for protoblock ' + protoblock);
  345. } else {
  346. blocks.protoBlockDict[protoblock].palette.add(blocks.protoBlockDict[protoblock]);
  347. }
  348. }
  349. palettes.updatePalettes();
  350. // Populate the lists of block types.
  351. blocks.findBlockTypes();
  352. // Return the object in case we need to save it to local storage.
  353. return obj;
  354. };
  355. function updatePluginObj(obj) {
  356. for (var name in obj['PALETTEPLUGINS']) {
  357. pluginObjs['PALETTEPLUGINS'][name] = obj['PALETTEPLUGINS'][name];
  358. }
  359. for (var name in obj['PALETTEFILLCOLORS']) {
  360. pluginObjs['PALETTEFILLCOLORS'][name] = obj['PALETTEFILLCOLORS'][name];
  361. }
  362. for (var name in obj['PALETTESTROKECOLORS']) {
  363. pluginObjs['PALETTESTROKECOLORS'][name] = obj['PALETTESTROKECOLORS'][name];
  364. }
  365. for (var name in obj['PALETTEHIGHLIGHTCOLORS']) {
  366. pluginObjs['PALETTEHIGHLIGHTCOLORS'][name] = obj['PALETTEHIGHLIGHTCOLORS'][name];
  367. }
  368. for (var flow in obj['FLOWPLUGINS']) {
  369. pluginObjs['FLOWPLUGINS'][flow] = obj['FLOWPLUGINS'][flow];
  370. }
  371. for (var arg in obj['ARGPLUGINS']) {
  372. pluginObjs['ARGPLUGINS'][arg] = obj['ARGPLUGINS'][arg];
  373. }
  374. for (var block in obj['BLOCKPLUGINS']) {
  375. pluginObjs['BLOCKPLUGINS'][block] = obj['BLOCKPLUGINS'][block];
  376. }
  377. if ('GLOBALS' in obj) {
  378. if (!('GLOBALS' in pluginObjs)) {
  379. pluginObjs['GLOBALS'] = '';
  380. }
  381. pluginObjs['GLOBALS'] += obj['GLOBALS'];
  382. }
  383. if ('IMAGES' in obj) {
  384. pluginObjs['IMAGES'] = obj['IMAGES'];
  385. }
  386. for (var name in obj['ONLOAD']) {
  387. pluginObjs['ONLOAD'][name] = obj['ONLOAD'][name];
  388. }
  389. for (var name in obj['ONSTART']) {
  390. pluginObjs['ONSTART'][name] = obj['ONSTART'][name];
  391. }
  392. for (var name in obj['ONSTOP']) {
  393. pluginObjs['ONSTOP'][name] = obj['ONSTOP'][name];
  394. }
  395. };
  396. function preparePluginExports(obj) {
  397. // add obj to plugin dictionary and return as JSON encoded text
  398. updatePluginObj(obj);
  399. return JSON.stringify(pluginObjs);
  400. };
  401. function processMacroData(macroData, palettes, blocks, macroDict) {
  402. // Macros are stored in a JSON-encoded dictionary.
  403. if (macroData !== '{}') {
  404. var obj = JSON.parse(macroData);
  405. palettes.add('myblocks', 'black', '#a0a0a0');
  406. for (var name in obj) {
  407. console.log('adding ' + name + ' to macroDict');
  408. macroDict[name] = obj[name];
  409. blocks.addToMyPalette(name, macroDict[name]);
  410. }
  411. palettes.makePalettes();
  412. }
  413. };
  414. function prepareMacroExports(name, stack, macroDict) {
  415. if (name !== null) {
  416. macroDict[name] = stack;
  417. }
  418. return JSON.stringify(macroDict);
  419. };
  420. function doSaveSVG(logo, desc) {
  421. var svg = doSVG(logo.canvas, logo, logo.turtles, logo.canvas.width, logo.canvas.height, 1.0);
  422. download(desc, 'data:image/svg+xml;utf8,' + svg, desc, '"width=' + logo.canvas.width + ', height=' + logo.canvas.height + '"');
  423. };
  424. function doSaveLilypond(logo, desc) {
  425. download(desc, 'data:text;utf8,' + encodeURIComponent(logo.lilypondOutput), desc, '"width=' + logo.canvas.width + ', height=' + logo.canvas.height + '"');
  426. };
  427. function download(filename, data) {
  428. var a = document.createElement('a');
  429. a.setAttribute('href', data);
  430. a.setAttribute('download', filename);
  431. document.body.appendChild(a);
  432. a.click();
  433. document.body.removeChild(a);
  434. };
  435. // Some block-specific code
  436. // Publish to FB
  437. function doPublish(desc) {
  438. var url = doSave();
  439. console.log('push ' + url + ' to FB');
  440. var descElem = docById("description");
  441. var msg = desc + ' ' + descElem.value + ' ' + url;
  442. console.log('comment: ' + msg);
  443. var post_cb = function() {
  444. FB.api('/me/feed', 'post', {
  445. message: msg
  446. });
  447. };
  448. FB.login(post_cb, {
  449. scope: 'publish_actions'
  450. });
  451. };
  452. // TODO: Move to camera plugin
  453. var hasSetupCamera = false;
  454. function doUseCamera(args, turtles, turtle, isVideo, cameraID, setCameraID, errorMsg) {
  455. var w = 320;
  456. var h = 240;
  457. var streaming = false;
  458. var video = document.querySelector('#camVideo');
  459. var canvas = document.querySelector('#camCanvas');
  460. navigator.getMedia = (navigator.getUserMedia ||
  461. navigator.mozGetUserMedia ||
  462. navigator.webkitGetUserMedia ||
  463. navigator.msGetUserMedia);
  464. if (navigator.getMedia === undefined) {
  465. errorMsg('Your browser does not support the webcam');
  466. }
  467. if (!hasSetupCamera) {
  468. navigator.getMedia(
  469. {video: true, audio: false},
  470. function (stream) {
  471. if (navigator.mozGetUserMedia) {
  472. video.mozSrcObject = stream;
  473. } else {
  474. var vendorURL = window.URL || window.webkitURL;
  475. video.src = vendorURL.createObjectURL(stream);
  476. }
  477. video.play();
  478. hasSetupCamera = true;
  479. }, function (error) {
  480. errorMsg('Could not connect to camera');
  481. console.log('Could not connect to camera', error);
  482. });
  483. } else {
  484. streaming = true;
  485. video.play();
  486. if (isVideo) {
  487. cameraID = window.setInterval(draw, 100);
  488. setCameraID(cameraID);
  489. } else {
  490. draw();
  491. }
  492. }
  493. video.addEventListener('canplay', function (event) {
  494. console.log('canplay', streaming, hasSetupCamera);
  495. if (!streaming) {
  496. video.setAttribute('width', w);
  497. video.setAttribute('height', h);
  498. canvas.setAttribute('width', w);
  499. canvas.setAttribute('height', h);
  500. streaming = true;
  501. if (isVideo) {
  502. cameraID = window.setInterval(draw, 100);
  503. setCameraID(cameraID);
  504. } else {
  505. draw();
  506. }
  507. }
  508. }, false);
  509. function draw() {
  510. canvas.width = w;
  511. canvas.height = h;
  512. canvas.getContext('2d').drawImage(video, 0, 0, w, h);
  513. var data = canvas.toDataURL('image/png');
  514. turtles.turtleList[turtle].doShowImage(args[0], data);
  515. };
  516. };
  517. function doStopVideoCam(cameraID, setCameraID) {
  518. if (cameraID !== null) {
  519. window.clearInterval(cameraID);
  520. }
  521. setCameraID(null);
  522. document.querySelector('#camVideo').pause();
  523. };
  524. function hideDOMLabel() {
  525. var textLabel = docById('textLabel');
  526. if (textLabel !== null) {
  527. textLabel.style.display = 'none';
  528. }
  529. var numberLabel = docById('numberLabel');
  530. if (numberLabel !== null) {
  531. numberLabel.style.display = 'none';
  532. }
  533. var solfegeLabel = docById('solfegeLabel');
  534. if (solfegeLabel !== null) {
  535. solfegeLabel.style.display = 'none';
  536. }
  537. var notenameLabel = docById('notenameLabel');
  538. if (notenameLabel !== null) {
  539. notenameLabel.style.display = 'none';
  540. }
  541. var noteattrLabel = docById('noteattrLabel');
  542. if (noteattrLabel !== null) {
  543. noteattrLabel.style.display = 'none';
  544. }
  545. var drumnameLabel = docById('drumnameLabel');
  546. if (drumnameLabel !== null) {
  547. drumnameLabel.style.display = 'none';
  548. }
  549. var voicenameLabel = docById('voicenameLabel');
  550. if (voicenameLabel !== null) {
  551. voicenameLabel.style.display = 'none';
  552. }
  553. var modenameLabel = docById('modenameLabel');
  554. if (modenameLabel !== null) {
  555. modenameLabel.style.display = 'none';
  556. }
  557. };
  558. function displayMsg(blocks, text) {
  559. /*
  560. var msgContainer = blocks.msgText.parent;
  561. msgContainer.visible = true;
  562. blocks.msgText.text = text;
  563. msgContainer.updateCache();
  564. blocks.stage.setChildIndex(msgContainer, blocks.stage.getNumChildren() - 1);
  565. */
  566. return;
  567. };
  568. function toFixed2 (d) {
  569. // Return number as fixed 2 precision
  570. var floor = Math.floor(d);
  571. if (d !== floor) {
  572. return d.toFixed(2).toString();
  573. } else {
  574. return d.toString();
  575. }
  576. };
  577. function mixedNumber (d) {
  578. // Return number as a mixed fraction string, e.g., "2 1/4"
  579. var floor = Math.floor(d);
  580. if (d > floor) {
  581. var obj = rationalToFraction(d - floor);
  582. if (floor === 0) {
  583. return obj[0] + '/' + obj[1];
  584. } else {
  585. if (obj[0] === 1 && obj[1] === 1) {
  586. return floor + 1;
  587. } else {
  588. if (obj[1] > 99) {
  589. return d.toFixed(2);
  590. } else {
  591. return floor + ' ' + obj[0] + '/' + obj[1];
  592. }
  593. }
  594. }
  595. } else {
  596. return d.toString();
  597. }
  598. };
  599. function LCD(a, b) {
  600. return Math.abs((a * b) / GCD(a, b));
  601. };
  602. function GCD(a, b) {
  603. a = Math.abs(a);
  604. b = Math.abs(b);
  605. while(b) {
  606. var n = b;
  607. b = a % b;
  608. a = n;
  609. }
  610. return a;
  611. };
  612. function rationalToFraction (d) {
  613. /*
  614. Convert float to its approximate fractional representation. '''
  615. This code was translated to JavaScript from the answers at
  616. http://stackoverflow.com/questions/95727/how-to-convert-floats-to-human-\
  617. readable-fractions/681534#681534
  618. For example:
  619. >>> 3./5
  620. 0.59999999999999998
  621. >>> rationalToFraction(3./5)
  622. "3/5"
  623. */
  624. if (d > 1) {
  625. var invert = true;
  626. d = 1 / d;
  627. } else {
  628. var invert = false;
  629. }
  630. var df = 1.0;
  631. var top = 1;
  632. var bot = 1;
  633. while (Math.abs(df - d) > 0.00000001) {
  634. if (df < d) {
  635. top += 1;
  636. } else {
  637. bot += 1;
  638. top = Math.floor(d * bot);
  639. }
  640. df = top / bot;
  641. }
  642. if (bot === 0 || top === 0) {
  643. return [0, 1];
  644. }
  645. if (invert) {
  646. return [bot, top];
  647. } else {
  648. return [top, bot];
  649. }
  650. };