graphics/graphics.js

  1. 'use strict';
  2. // Adding Array Methods
  3. Array.prototype.remove = function(idx) {
  4. return this.splice(idx, 1)[0];
  5. };
  6. // import the npm-hosted editor utils only if the other is not available
  7. var editorUtils = require('codehs-js-utils');
  8. // import graphics utilities functions
  9. var graphicsUtils = require('./graphics-utils.js');
  10. // import audio context utils
  11. var getAudioContext = require('./audioContext.js');
  12. // How often to redraw the display
  13. var DEFAULT_FRAME_RATE = 40;
  14. // Padding between graphics canvas and parent element when in fullscreenMode
  15. var FULLSCREEN_PADDING = 5;
  16. // String list of methods that will be accessible
  17. // to the user
  18. var PUBLIC_METHODS = [];
  19. var PUBLIC_CONSTRUCTORS = [];
  20. // Pressed keys are actually maintained acorss all
  21. // graphics instances since there is only one keyboard.
  22. var pressedKeys = [];
  23. // Keep track of all graphics instances.
  24. var allGraphicsInstances = [];
  25. var graphicsInstanceId = 0;
  26. var analyser;
  27. var dataArray;
  28. var gainNode;
  29. var source;
  30. var audioCtx = 0;
  31. /**
  32. * Set up an instance of the graphics library.
  33. * @constructor
  34. * @param {dictionary} options - Options, primarily .canvas, the selector
  35. * string for the canvas.
  36. * If multiple are returned, we'll take the first one.
  37. * If none is passed, we'll look for any canvas
  38. * tag on the page.
  39. */
  40. function CodeHSGraphics(options) {
  41. options = options || {};
  42. this.resetAllState();
  43. this.globalTimer = true;
  44. this.currentCanvas = null;
  45. this.setCurrentCanvas(options.canvas);
  46. // Are we in debug mode? The default is false.
  47. this.debugMode = options.debug || false;
  48. this.fullscreenMode = false;
  49. // Since we now have multiple instances of the graphics object
  50. // give each one a unique id
  51. this.instanceId = graphicsInstanceId;
  52. graphicsInstanceId++;
  53. // override any graphics instance that is already using this ID.
  54. // if there aren't any, just push this instance onto the end.
  55. var existingId = this.canvasHasInstance(options.canvas);
  56. if (existingId !== null) {
  57. var existingGraphics = allGraphicsInstances[existingId];
  58. existingGraphics.stopTimer('MAIN_TIMER');
  59. allGraphicsInstances[existingId] = this;
  60. } else {
  61. allGraphicsInstances.push(this);
  62. }
  63. }
  64. /**
  65. * Adds a method to the public methods constant.
  66. * @param {string} name - Name of the method.
  67. */
  68. CodeHSGraphics.registerPublicMethod = function(name) {
  69. PUBLIC_METHODS.push(name);
  70. };
  71. /**
  72. * Adds a constructor to the public constructors constant.
  73. * @param {string} name - Name of the object to be constructed.
  74. */
  75. CodeHSGraphics.registerConstructorMethod = function(name) {
  76. PUBLIC_CONSTRUCTORS.push(name);
  77. };
  78. /**
  79. * Generate strings for the public methods to bring them to the
  80. * public namespace without having to call them with the graphics instance.
  81. * @returns {string} Line broken function definitions.
  82. */
  83. CodeHSGraphics.getNamespaceModifcationString = function() {
  84. var result = '\n';
  85. for (var i = 0; i < PUBLIC_METHODS.length; i++) {
  86. var curMethod = PUBLIC_METHODS[i];
  87. // Actually create a method in this scope with the name of the
  88. // method so the student can easily access it. For example, we
  89. // might have a method like CodeHSGraphics.prototype.add, but we
  90. // want the student to be able to access it with just `add`, but
  91. // the proper context for this.
  92. result +=
  93. 'function ' +
  94. curMethod +
  95. '(){\n' +
  96. '\treturn __graphics__.' +
  97. curMethod +
  98. '.apply(__graphics__, arguments);\n' +
  99. '}\n';
  100. // result += 'var ' + curMethod + ' = __graphics__.' + curMethod + ';\n';
  101. }
  102. return result;
  103. };
  104. /**
  105. * Generate strings for the public constructors to bring them to the
  106. * public namespace without having to call them with the graphics instance.
  107. * @returns {string} Line broken constructor declarations.
  108. */
  109. CodeHSGraphics.getConstructorModificationString = function() {
  110. var result = '';
  111. for (var i = 0; i < PUBLIC_CONSTRUCTORS.length; i++) {
  112. var curMethod = PUBLIC_CONSTRUCTORS[i];
  113. result += 'var ' + curMethod + ' = __graphics__.' + curMethod + ';\n';
  114. }
  115. return result;
  116. };
  117. /** ************* PUBLIC METHODS *******************/
  118. // NOTE: if you add a public method, you MUST fix linenumber calc for errors:
  119. // function getCorrectLineNumber in editorErrors.js
  120. // adding a public method will add 3 lines to the program.
  121. /**
  122. * Add an element to the graphics instance.
  123. * @param {Thing} elem - A subclass of Thing to be added to the graphics instance.
  124. */
  125. CodeHSGraphics.prototype.add = function(elem) {
  126. this.elements.push(elem);
  127. };
  128. CodeHSGraphics.registerPublicMethod('add');
  129. /**
  130. * Wrapper around Audio so we have reference to all Audio objects created.
  131. * @param{String} url - url of the audio file.
  132. */
  133. window.oldAudio = window.Audio;
  134. CodeHSGraphics.prototype.Audio = function(url) {
  135. var audioElem = new oldAudio(url);
  136. audioElem.crossOrigin = 'anonymous';
  137. this.audioElements.push(audioElem);
  138. return audioElem;
  139. };
  140. CodeHSGraphics.prototype.Audio.constructor = window.oldAudio;
  141. CodeHSGraphics.registerPublicMethod('Audio');
  142. /**
  143. * Wrapper around Sound so we have reference to all Sound objects created.
  144. * Following the example set by tracking Audio elements.
  145. * @param frequency - Either a number (Hertz) or note ("C#4" for middle C Sharp)
  146. * @param oscillatorType {string} - several options
  147. * basic types: "sine", "triangle", "square", "sawtooth"
  148. * any basic type can be prefixed with "fat", "am" or "fm", ie "fatsawtooth"
  149. * any basic type can be suffixed with a number ie "4" for the number of partials
  150. * ie "square4"
  151. * special types: "pwm", "pulse"
  152. * drum instrument: "membrane"
  153. * cymbal instrument: "metal"
  154. * https://tonejs.github.io/docs/13.8.25/OmniOscillator
  155. */
  156. var oldSound = require('./sound.js');
  157. CodeHSGraphics.prototype.Sound = function(frequency, oscillatorType) {
  158. frequency = frequency || 440;
  159. oscillatorType = oscillatorType || 'fatsawtooth';
  160. var soundElem = new oldSound(frequency, oscillatorType);
  161. this.soundElements.push(soundElem);
  162. return soundElem;
  163. };
  164. CodeHSGraphics.prototype.Sound.constructor = oldSound;
  165. CodeHSGraphics.registerPublicMethod('Sound');
  166. /**
  167. * Record a click.
  168. */
  169. CodeHSGraphics.prototype.waitForClick = function() {
  170. this.clickCount++;
  171. };
  172. CodeHSGraphics.registerPublicMethod('waitForClick');
  173. /**
  174. * Assign a function as a callback for click (mouse down, mouse up) events.
  175. * @param {function} fn - A callback to be triggered on click events.
  176. */
  177. CodeHSGraphics.prototype.mouseClickMethod = function(fn) {
  178. this.clickCallback = editorUtils.safeCallback(fn);
  179. };
  180. CodeHSGraphics.registerPublicMethod('mouseClickMethod');
  181. /**
  182. * Assign a function as a callback for mouse move events.
  183. * @param {function} fn - A callback to be triggered on mouse move events.
  184. */
  185. CodeHSGraphics.prototype.mouseMoveMethod = function(fn) {
  186. this.moveCallback = editorUtils.safeCallback(fn);
  187. };
  188. CodeHSGraphics.registerPublicMethod('mouseMoveMethod');
  189. /**
  190. * Assign a function as a callback for mouse down events.
  191. * @param {function} fn - A callback to be triggered on mouse down.
  192. */
  193. CodeHSGraphics.prototype.mouseDownMethod = function(fn) {
  194. this.mouseDownCallback = editorUtils.safeCallback(fn);
  195. };
  196. CodeHSGraphics.registerPublicMethod('mouseDownMethod');
  197. /**
  198. * Assign a function as a callback for mouse up events.
  199. * @param {function} fn - A callback to be triggered on mouse up events.
  200. */
  201. CodeHSGraphics.prototype.mouseUpMethod = function(fn) {
  202. this.mouseUpCallback = editorUtils.safeCallback(fn);
  203. };
  204. CodeHSGraphics.registerPublicMethod('mouseUpMethod');
  205. /**
  206. * Assign a function as a callback for drag events.
  207. * @param {function} fn - A callback to be triggered on drag events.
  208. */
  209. CodeHSGraphics.prototype.mouseDragMethod = function(fn) {
  210. this.dragCallback = editorUtils.safeCallback(fn);
  211. };
  212. CodeHSGraphics.registerPublicMethod('mouseDragMethod');
  213. /**
  214. * Assign a function as a callback for keydown events.
  215. * @param {function} fn - A callback to be triggered on keydown events.
  216. */
  217. CodeHSGraphics.prototype.keyDownMethod = function(fn) {
  218. this.keyDownCallback = editorUtils.safeCallback(fn);
  219. };
  220. CodeHSGraphics.registerPublicMethod('keyDownMethod');
  221. /**
  222. * Assign a function as a callback for key up events.
  223. * @param {function} fn - A callback to be triggered on key up events.
  224. */
  225. CodeHSGraphics.prototype.keyUpMethod = function(fn) {
  226. this.keyUpCallback = editorUtils.safeCallback(fn);
  227. };
  228. CodeHSGraphics.registerPublicMethod('keyUpMethod');
  229. /**
  230. * Assign a function as a callback for device orientation events.
  231. * @param {function} fn - A callback to be triggered on device orientation
  232. * events.
  233. */
  234. CodeHSGraphics.prototype.deviceOrientationMethod = function(fn) {
  235. this.deviceOrientationCallback = editorUtils.safeCallback(fn);
  236. };
  237. CodeHSGraphics.registerPublicMethod('deviceOrientationMethod');
  238. /**
  239. * Assign a function as a callback for device motion events.
  240. * @param {function} fn - A callback to be triggered device motion events.
  241. */
  242. CodeHSGraphics.prototype.deviceMotionMethod = function(fn) {
  243. this.deviceMotionCallback = editorUtils.safeCallback(fn);
  244. };
  245. CodeHSGraphics.registerPublicMethod('deviceMotionMethod');
  246. /**
  247. * Assign a function as a callback for when audio data changes for audio
  248. * being played in a graphics program.
  249. * @param {object} tag - Audio element playing sound to analyze
  250. * @param {function} fn - A callback to be triggered on audio data change.
  251. */
  252. CodeHSGraphics.prototype.audioChangeMethod = function(tag, fn) {
  253. // get new audio context and create analyser
  254. audioCtx = getAudioContext();
  255. // IE browser exit gracefully
  256. if (!audioCtx) {
  257. return;
  258. }
  259. analyser = audioCtx.createAnalyser();
  260. // set fft -- used to set the number of slices we break our frequency range
  261. // in to.
  262. analyser.fftSize = 128;
  263. // gt bugger length and create a new array in that size
  264. var bufferLength = analyser.frequencyBinCount;
  265. dataArray = new Uint8Array(bufferLength);
  266. // create media source from student's audio tag
  267. source = audioCtx.createMediaElementSource(tag);
  268. // should allow cors
  269. source.crossOrigin = 'anonymous';
  270. // connect analyzer to sound
  271. source.connect(analyser);
  272. // create gain node and connect to sound (makes speaker output possuble)
  273. var gainNode = audioCtx.createGain();
  274. source.connect(gainNode);
  275. gainNode.connect(audioCtx.destination);
  276. // create callback fn and assign attach to timer
  277. this.audioChangeCallback = editorUtils.safeCallback(fn);
  278. this.setGraphicsTimer(this.updateAudio.bind(this), DEFAULT_FRAME_RATE, null, 'updateAudio');
  279. };
  280. CodeHSGraphics.registerPublicMethod('audioChangeMethod');
  281. /**
  282. * Check if a key is currently pressed
  283. * @param {integer} keyCode - Key code of key being checked.
  284. * @returns {boolean} Whether or not that key is being pressed.
  285. */
  286. CodeHSGraphics.prototype.isKeyPressed = function(keyCode) {
  287. return pressedKeys.indexOf(keyCode) != -1;
  288. };
  289. CodeHSGraphics.registerPublicMethod('isKeyPressed');
  290. /**
  291. * Get the width of the entire graphics canvas.
  292. * @returns {float} The width of the canvas.
  293. */
  294. CodeHSGraphics.prototype.getWidth = function() {
  295. var canvas = this.getCanvas();
  296. return parseFloat(canvas.getAttribute('width'));
  297. };
  298. CodeHSGraphics.registerPublicMethod('getWidth');
  299. /**
  300. * Get the height of the entire graphics canvas.
  301. * @returns {float} The height of the canvas.
  302. */
  303. CodeHSGraphics.prototype.getHeight = function() {
  304. var canvas = this.getCanvas();
  305. return parseFloat(canvas.getAttribute('height'));
  306. };
  307. CodeHSGraphics.registerPublicMethod('getHeight');
  308. /**
  309. * Remove a timer associated with a function.
  310. * @param {function} fn - Function whose timer is removed.
  311. * note 'fn' may also be the name of the function.
  312. */
  313. CodeHSGraphics.prototype.stopTimer = function(fn) {
  314. var key = typeof fn === 'function' ? fn.name : fn;
  315. clearInterval(this.timers[key]);
  316. };
  317. CodeHSGraphics.registerPublicMethod('stopTimer');
  318. /**
  319. * Stop all timers.
  320. */
  321. CodeHSGraphics.prototype.stopAllTimers = function() {
  322. for (var i = 1; i < 99999; i++) {
  323. window.clearInterval(i);
  324. }
  325. this.setMainTimer();
  326. };
  327. CodeHSGraphics.registerPublicMethod('stopAllTimers');
  328. /**
  329. * Create a new timer
  330. * @param {function} fn - Function to be called at intervals.
  331. * @param {integer} time - Time interval to call function `fn`
  332. * @param {dictionary} data - Any data associated with the timer.
  333. * @param {string} name - Name of this timer.
  334. */
  335. CodeHSGraphics.prototype.setTimer = function(fn, time, data, name) {
  336. if (arguments.length < 2) {
  337. throw new Error(
  338. '2 parameters required for <span class="code">' +
  339. 'setTimer</span>, ' +
  340. arguments.length +
  341. ' found. You must ' +
  342. 'provide a callback function and ' +
  343. 'a number representing the time delay ' +
  344. 'to <span class="code">setTimer</span>'
  345. );
  346. }
  347. if (typeof fn !== 'function') {
  348. throw new TypeError(
  349. 'Invalid callback function. ' +
  350. 'Make sure you are passing an actual function to ' +
  351. '<span class="code">setTimer</span>.'
  352. );
  353. }
  354. if (typeof time !== 'number' || !isFinite(time)) {
  355. throw new TypeError(
  356. 'Invalid value for time delay. ' +
  357. 'Make sure you are passing a finite number to ' +
  358. '<span class="code">setTimer</span> for the delay.'
  359. );
  360. }
  361. var self = this;
  362. // Safety, set a min frequency
  363. if (isNaN(time) || time < 15) {
  364. time = 15;
  365. }
  366. if (this.waitingForClick()) {
  367. this.delayedTimers.push({
  368. fn: fn,
  369. time: time,
  370. data: data,
  371. clicks: self.clickCount,
  372. name: name,
  373. });
  374. } else {
  375. this.setGraphicsTimer(fn, time, data, name);
  376. }
  377. };
  378. CodeHSGraphics.registerPublicMethod('setTimer');
  379. /**
  380. * Set the background color of the canvas.
  381. * @param {Color} color - The desired color of the canvas.
  382. */
  383. CodeHSGraphics.prototype.setBackgroundColor = function(color) {
  384. this.backgroundColor = color;
  385. };
  386. CodeHSGraphics.registerPublicMethod('setBackgroundColor');
  387. /**
  388. * Clear everything from the canvas.
  389. */
  390. CodeHSGraphics.prototype.clear = function(context) {
  391. var ctx = context || this.getContext();
  392. ctx.clearRect(0, 0, this.getWidth(), this.getHeight());
  393. };
  394. CodeHSGraphics.registerPublicMethod('clear');
  395. /**
  396. * Get an element at a specific point.
  397. * If several elements are present at the position, return the one put there first.
  398. * @param {number} x - The x coordinate of a point to get element at.
  399. * @param {number} y - The y coordinate of a point to get element at.
  400. * @returns {Thing|null} The object at the point (x, y), if there is one (else null).
  401. */
  402. CodeHSGraphics.prototype.getElementAt = function(x, y) {
  403. for (var i = this.elements.length - 1; i >= 0; i--) {
  404. if (this.elements[i].containsPoint(x, y, this)) {
  405. return this.elements[i];
  406. }
  407. }
  408. return null;
  409. };
  410. CodeHSGraphics.registerPublicMethod('getElementAt');
  411. /**
  412. * Check if an element exists with the given paramenters.
  413. * @param {object} params - Dictionary of parameters for the object.
  414. * Includes x, y, heigh, width, color, radius, label and type.
  415. * @returns {boolean}
  416. */
  417. CodeHSGraphics.prototype.elementExistsWithParameters = function(params) {
  418. for (var i = this.elements.length - 1; i >= 0; i--) {
  419. var elem = this.elements[i];
  420. try {
  421. if (
  422. params.x !== undefined &&
  423. this.runCode('return ' + params.x).result.toFixed(0) != elem.getX().toFixed(0)
  424. ) {
  425. continue;
  426. }
  427. if (
  428. params.y !== undefined &&
  429. this.runCode('return ' + params.y).result.toFixed(0) != elem.getY().toFixed(0)
  430. ) {
  431. continue;
  432. }
  433. if (
  434. params.width !== undefined &&
  435. this.runCode('return ' + params.width).result.toFixed(0) !=
  436. elem.getWidth().toFixed(0)
  437. ) {
  438. continue;
  439. }
  440. if (
  441. params.height !== undefined &&
  442. this.runCode('return ' + params.height).result.toFixed(0) !=
  443. elem.getHeight().toFixed(0)
  444. ) {
  445. continue;
  446. }
  447. if (
  448. params.radius !== undefined &&
  449. this.runCode('return ' + params.radius).result.toFixed(0) !=
  450. elem.getRadius().toFixed(0)
  451. ) {
  452. continue;
  453. }
  454. if (
  455. params.color !== undefined &&
  456. this.runCode('return ' + params.color).result != elem.getColor()
  457. ) {
  458. continue;
  459. }
  460. if (params.label !== undefined && params.label != elem.getLabel()) {
  461. continue;
  462. }
  463. if (params.type !== undefined && params.type != elem.getType()) {
  464. continue;
  465. }
  466. } catch (err) {
  467. continue;
  468. }
  469. return true;
  470. }
  471. return false;
  472. };
  473. CodeHSGraphics.registerPublicMethod('elementExistsWithParameters');
  474. /**
  475. * Remove all elements from the canvas.
  476. */
  477. CodeHSGraphics.prototype.removeAll = function() {
  478. this.stopAllVideo();
  479. this.elements = [];
  480. };
  481. CodeHSGraphics.registerPublicMethod('removeAll');
  482. /**
  483. * Remove a specific element from the canvas.
  484. * @param {Thing} elem - The element to be removed from the canvas.
  485. */
  486. CodeHSGraphics.prototype.remove = function(elem) {
  487. for (var i = 0; i < this.elements.length; i++) {
  488. if (this.elements[i] == elem) {
  489. if (this.elements[i].type == 'WebVideo') {
  490. this.elements[i].stop();
  491. }
  492. this.elements.splice(i, 1); // Remove from list
  493. }
  494. }
  495. };
  496. CodeHSGraphics.registerPublicMethod('remove');
  497. /**
  498. * Set the size of the canvas.
  499. * @param {number} w - Desired width of the canvas.
  500. * @param {number} h - Desired height of the canvas.
  501. */
  502. CodeHSGraphics.prototype.setSize = function(w, h) {
  503. this.fullscreenMode = false;
  504. var canvas = this.getCanvas();
  505. canvas.width = w;
  506. canvas.height = h;
  507. $(canvas).css({
  508. 'max-height': h,
  509. 'max-width': w,
  510. });
  511. };
  512. CodeHSGraphics.registerPublicMethod('setSize');
  513. /**
  514. * Set the canvas to take up the entire parent element
  515. */
  516. CodeHSGraphics.prototype.setFullscreen = function() {
  517. var self = this;
  518. self.fullscreenMode = true; // when this is true, canvas will resize with parent
  519. var canvas = this.getCanvas();
  520. canvas.width = canvas.parentElement.offsetWidth - FULLSCREEN_PADDING;
  521. canvas.height = canvas.parentElement.offsetHeight - FULLSCREEN_PADDING;
  522. $(canvas).css({
  523. 'max-height': canvas.height,
  524. 'max-width': canvas.width,
  525. });
  526. };
  527. CodeHSGraphics.registerPublicMethod('setFullscreen');
  528. /** **************** SHAPE CONSTRUCTORS **************/
  529. // Insertion point for graphics modules.
  530. CodeHSGraphics.prototype.Rectangle = require('./rectangle.js');
  531. CodeHSGraphics.registerConstructorMethod('Rectangle');
  532. CodeHSGraphics.prototype.Circle = require('./circle.js');
  533. CodeHSGraphics.registerConstructorMethod('Circle');
  534. CodeHSGraphics.prototype.Line = require('./line.js');
  535. CodeHSGraphics.registerConstructorMethod('Line');
  536. CodeHSGraphics.prototype.Grid = require('./grid.js');
  537. CodeHSGraphics.registerConstructorMethod('Grid');
  538. CodeHSGraphics.prototype.Line = require('./line.js');
  539. CodeHSGraphics.registerConstructorMethod('Line');
  540. CodeHSGraphics.prototype.Polygon = require('./polygon.js');
  541. CodeHSGraphics.registerConstructorMethod('Polygon');
  542. CodeHSGraphics.prototype.Text = require('./text.js');
  543. CodeHSGraphics.registerConstructorMethod('Text');
  544. CodeHSGraphics.prototype.Oval = require('./oval.js');
  545. CodeHSGraphics.registerConstructorMethod('Oval');
  546. CodeHSGraphics.prototype.Arc = require('./arc.js');
  547. CodeHSGraphics.registerConstructorMethod('Arc');
  548. CodeHSGraphics.prototype.Color = require('./color.js');
  549. CodeHSGraphics.registerConstructorMethod('Color');
  550. CodeHSGraphics.prototype.WebImage = require('./webimage.js');
  551. CodeHSGraphics.registerConstructorMethod('WebImage');
  552. CodeHSGraphics.prototype.WebVideo = require('./webvideo.js');
  553. CodeHSGraphics.registerConstructorMethod('WebVideo');
  554. CodeHSGraphics.prototype.ImageLibrary = require('./imagelibrary.js');
  555. CodeHSGraphics.registerConstructorMethod('ImageLibrary');
  556. /** **************** PRIVATE METHODS *****************/
  557. /**
  558. * This is how you run the code, but get access to the
  559. * state of the graphics library. The current instance
  560. * becomes accessible in the code.
  561. * @param {string} code - The code from the editor.
  562. */
  563. CodeHSGraphics.prototype.runCode = function(code, options) {
  564. options = options || {};
  565. var getPublicMethodString = CodeHSGraphics.getNamespaceModifcationString();
  566. var getConstructorModificationString = CodeHSGraphics.getConstructorModificationString();
  567. var wrap = '';
  568. // Give the user easy access to public graphics methods
  569. // in the proper context.
  570. wrap += getPublicMethodString;
  571. wrap += getConstructorModificationString;
  572. // Set up `Text` so we don't need to redefine it on the window
  573. wrap += ';var __nativeText=window.Text;var Text=__graphics__.Text;';
  574. // Text objects need access to some 2d graphics context to compute
  575. // height and width. This might be done before a draw call.
  576. wrap += '\nText.giveDefaultContext(__graphics__);\n';
  577. // Set up `Set`
  578. wrap += ';var __nativeSet=window.Set;var Set=window.chsSet;';
  579. if (!options.overrideInfiniteLoops) {
  580. // tool all while loops
  581. var whileLoopRegEx = /while\s*\((.*)\)\s*{/gm;
  582. var forLoopRegEx = /for\s*\((.*)\)\s*{/gm;
  583. var doWhileRegEx = /do\s*\{/gm;
  584. // Inject into while loops
  585. code = code.replace(whileLoopRegEx, function(match, p1, offset, string) {
  586. var lineNumber = string.slice(0, offset).split('\n').length;
  587. var c =
  588. "if(___nloops++>15000){var e = new Error('Your while loop on line " +
  589. lineNumber +
  590. " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " +
  591. lineNumber +
  592. '; throw e;}';
  593. return 'var ___nloops=0;while(' + p1 + ') {' + c;
  594. });
  595. // Inject into for loops
  596. code = code.replace(forLoopRegEx, function(match, p1, offset, string) {
  597. var lineNumber = string.slice(0, offset).split('\n').length;
  598. var c =
  599. "if(___nloops++>15000){var e = new Error('Your for loop on line " +
  600. lineNumber +
  601. " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " +
  602. lineNumber +
  603. '; throw e;}';
  604. return 'var ___nloops=0;for(' + p1 + '){' + c;
  605. });
  606. // Inject into do-while loops
  607. code = code.replace(doWhileRegEx, function(match, offset, string) {
  608. var lineNumber = string.slice(0, offset).split('\n').length;
  609. var c =
  610. "if(___nloops++>15000){var e = new Error('Your do-while loop on line " +
  611. lineNumber +
  612. " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " +
  613. lineNumber +
  614. '; throw e;}';
  615. return 'var ___nloops=0;do {' + c;
  616. });
  617. }
  618. // User code.
  619. wrap += code;
  620. // Call the start function
  621. wrap += "\n\nif(typeof start == 'function') {start();} ";
  622. wrap += ';window.Text=__nativeText;';
  623. wrap += ';window.Set=__nativeSet;';
  624. return editorUtils.safeEval(wrap, this, '__graphics__');
  625. };
  626. /**
  627. * Resets all the timers to time 0.
  628. */
  629. CodeHSGraphics.prototype.resetAllTimers = function() {
  630. for (var cur in this.timers) {
  631. clearInterval(this.timers[cur]);
  632. }
  633. };
  634. CodeHSGraphics.prototype.stopAllAudio = function() {
  635. this.audioElements.forEach(function(audio) {
  636. audio.pause();
  637. });
  638. this.soundElements.forEach(function(soundElem) {
  639. soundElem.stop();
  640. soundElem.disconnect();
  641. });
  642. };
  643. CodeHSGraphics.prototype.stopAllVideo = function() {
  644. for (var i = 0; i < this.elements.length; i++) {
  645. if (this.elements[i].type == 'WebVideo') {
  646. this.elements[i].stop();
  647. }
  648. }
  649. };
  650. /**
  651. * Resets the graphics instance to a clean slate.
  652. */
  653. CodeHSGraphics.prototype.resetAllState = function() {
  654. this.backgroundColor = null;
  655. this.elements = [];
  656. this.audioElements = [];
  657. this.soundElements = [];
  658. this.clickCallback = null;
  659. this.moveCallback = null;
  660. this.mouseDownCallback = null;
  661. this.mouseUpCallback = null;
  662. this.dragCallback = null;
  663. this.keyDownCallback = null;
  664. this.keyUpCallback = null;
  665. this.deviceOrientationCallback = null;
  666. this.deviceMotionCallback = null;
  667. this.audioChangeCallback = null;
  668. // if audio source exists, disconnect it
  669. if (source) {
  670. source.disconnect();
  671. source = 0;
  672. }
  673. // A fast hash from timer key to timer interval #
  674. this.timers = {};
  675. // A useful list to store information about all timers.
  676. this.timersList = [];
  677. this.clickCount = 0;
  678. this.delayedTimers = [];
  679. // if audio context exists, close it and reset audioCtx
  680. if (audioCtx) {
  681. audioCtx.close();
  682. audioCtx = 0;
  683. }
  684. this.fullscreenMode = false;
  685. };
  686. /**
  687. * Reset all timers to 0 and clear timers and canvas.
  688. */
  689. CodeHSGraphics.prototype.fullReset = function() {
  690. this.stopAllAudio();
  691. this.stopAllVideo();
  692. this.resetAllTimers();
  693. this.resetAllState();
  694. /* THIS LINE OF CODE. Leave it commented out.
  695. * If we override this setting ( like we do in karel)
  696. * it shouldn't be reset to true. */
  697. // this.globalTimer = true;
  698. this.setMainTimer();
  699. };
  700. /**
  701. * Return if the graphics canvas exists.
  702. * @returns {boolean} Whether or not the canvas exists.
  703. */
  704. CodeHSGraphics.prototype.canvasExists = function() {
  705. return this.getCanvas() !== null;
  706. };
  707. /**
  708. * Return the current canvas we are using. If there is no
  709. * canvas on the page this will return null.
  710. * @returns {object} The current canvas.
  711. */
  712. CodeHSGraphics.prototype.getCanvas = function() {
  713. return this.currentCanvas;
  714. };
  715. /**
  716. * Set the current canvas we are working with. If no canvas
  717. * tag matches the selectorv then we will just have the current
  718. * canvas set to null.
  719. * @param {string} canvasSelector - String representing canvas class or ID.
  720. * Selected with jQuery.
  721. */
  722. CodeHSGraphics.prototype.setCurrentCanvas = function(canvasSelector) {
  723. /* If we were passed a selector, get the first matching
  724. * element. */
  725. if (canvasSelector) {
  726. this.currentCanvas = $(canvasSelector)[0];
  727. } else {
  728. this.currentCanvas = document.getElementsByTagName('canvas')[0];
  729. }
  730. // If it is a falsey value like undefined, set it to null.
  731. if (!this.currentCanvas) {
  732. this.currentCanvas = null;
  733. }
  734. // On changing the canvas reset the state.
  735. this.fullReset();
  736. this.setup();
  737. };
  738. /**
  739. * Stop the global timer
  740. */
  741. CodeHSGraphics.prototype.stopGlobalTimer = function() {
  742. this.globalTimer = false;
  743. };
  744. /**
  745. * Draw the background color for the current object.
  746. */
  747. CodeHSGraphics.prototype.drawBackground = function() {
  748. if (this.backgroundColor) {
  749. var context = this.getContext();
  750. context.fillStyle = this.backgroundColor;
  751. context.beginPath();
  752. context.rect(0, 0, this.getWidth(), this.getHeight());
  753. context.closePath();
  754. context.fill();
  755. }
  756. };
  757. /**
  758. * Return the 2D graphics context for this graphics
  759. * object, or null if none exists.
  760. * @returns {context} The 2D graphics context.
  761. */
  762. CodeHSGraphics.prototype.getContext = function() {
  763. var drawingCanvas = this.getCanvas();
  764. // Check the element is in the DOM and the browser supports canvas
  765. if (drawingCanvas && drawingCanvas.getContext) {
  766. // Initaliase a 2-dimensional drawing context
  767. var context = drawingCanvas.getContext('2d');
  768. return context;
  769. }
  770. return null;
  771. };
  772. /**
  773. * Redraw this graphics canvas.
  774. */
  775. CodeHSGraphics.prototype.redraw = function() {
  776. this.clear();
  777. this.drawBackground();
  778. for (var i = 0; i < this.elements.length; i++) {
  779. this.elements[i].draw(this);
  780. }
  781. };
  782. /**
  783. * Set the main timer for graphics.
  784. */
  785. CodeHSGraphics.prototype.setMainTimer = function() {
  786. var self = this;
  787. /* Refresh the screen every 40 ms */
  788. if (this.globalTimer) {
  789. this.setTimer(
  790. function() {
  791. self.redraw();
  792. },
  793. DEFAULT_FRAME_RATE,
  794. null,
  795. 'MAIN_TIMER'
  796. );
  797. }
  798. };
  799. /**
  800. * Whether the graphics instance is waiting for a click.
  801. * @returns {boolean} Whether or not the instance is waiting for a click.
  802. */
  803. CodeHSGraphics.prototype.waitingForClick = function() {
  804. return this.clickCount !== 0;
  805. };
  806. /**
  807. * Whether the selected canvas already has an instance associated.
  808. */
  809. CodeHSGraphics.prototype.canvasHasInstance = function(canvas) {
  810. var instance;
  811. for (var i = 0; i < allGraphicsInstances.length; i++) {
  812. instance = allGraphicsInstances[i];
  813. if (instance.instanceId !== this.instanceId && instance.getCanvas() === canvas) {
  814. return instance.instanceId;
  815. }
  816. }
  817. return null;
  818. };
  819. /**
  820. * Get the distance between two points, (x1, y1) and (x2, y2)
  821. * @param {number} x1
  822. * @param {number} y1
  823. * @param {number} x2
  824. * @param {number} y2
  825. * @returns {number} Distance between the two points.
  826. */
  827. CodeHSGraphics.prototype.getDistance = function(x1, y1, x2, y2) {
  828. return graphicsUtils.getDistance(x1, y1, x2, y2);
  829. };
  830. /**
  831. * Set up the graphics instance to prepare for interaction
  832. */
  833. CodeHSGraphics.prototype.setup = function() {
  834. var self = this;
  835. var drawingCanvas = this.getCanvas();
  836. // self.setMainTimer();
  837. drawingCanvas.onclick = function(e) {
  838. if (self.waitingForClick()) {
  839. self.clickCount--;
  840. for (var i = 0; i < self.delayedTimers.length; i++) {
  841. var timer = self.delayedTimers[i];
  842. timer.clicks--;
  843. if (timer.clicks === 0) {
  844. self.setGraphicsTimer(timer.fn, timer.time, timer.data);
  845. }
  846. }
  847. return;
  848. }
  849. if (self.clickCallback) {
  850. self.clickCallback(e);
  851. }
  852. };
  853. var mouseDown = false;
  854. drawingCanvas.onmousemove = function(e) {
  855. if (self.moveCallback) {
  856. self.moveCallback(e);
  857. }
  858. if (mouseDown && self.dragCallback) {
  859. self.dragCallback(e);
  860. }
  861. };
  862. drawingCanvas.onmousedown = function(e) {
  863. mouseDown = true;
  864. if (self.mouseDownCallback) {
  865. self.mouseDownCallback(e);
  866. }
  867. };
  868. drawingCanvas.onmouseup = function(e) {
  869. mouseDown = false;
  870. if (self.mouseUpCallback) {
  871. self.mouseUpCallback(e);
  872. }
  873. };
  874. // TOUCH EVENTS!
  875. drawingCanvas.ontouchmove = function(e) {
  876. e.preventDefault();
  877. if (self.dragCallback) {
  878. self.dragCallback(e);
  879. } else if (self.moveCallback) {
  880. self.moveCallback(e);
  881. }
  882. };
  883. drawingCanvas.ontouchstart = function(e) {
  884. e.preventDefault();
  885. if (self.mouseDownCallback) {
  886. self.mouseDownCallback(e);
  887. } else if (self.clickCallback) {
  888. self.clickCallback(e);
  889. }
  890. if (self.waitingForClick()) {
  891. self.clickCount--;
  892. for (var i = 0; i < self.delayedTimers.length; i++) {
  893. var timer = self.delayedTimers[i];
  894. timer.clicks--;
  895. if (timer.clicks === 0) {
  896. self.setGraphicsTimer(timer.fn, timer.time, timer.data);
  897. }
  898. }
  899. return;
  900. }
  901. };
  902. drawingCanvas.ontouchend = function(e) {
  903. e.preventDefault();
  904. if (self.mouseUpCallback) {
  905. self.mouseUpCallback(e);
  906. }
  907. };
  908. };
  909. /**
  910. * Set a graphics timer.
  911. * @param {function} fn - The function to be executed on the timer.
  912. * @param {number} time - The time interval for the function.
  913. * @param {object} data - Any arguments to be passed into `fn`.
  914. * @param {string} name - The name of the timer.
  915. */
  916. CodeHSGraphics.prototype.setGraphicsTimer = function(fn, time, data, name) {
  917. if (typeof name === 'undefined') {
  918. name = fn.name;
  919. }
  920. this.timers[name] = editorUtils.safeSetInterval(fn, data, time);
  921. this.timersList.push({
  922. name: name,
  923. fn: fn,
  924. data: data,
  925. time: time,
  926. });
  927. };
  928. /** AUDIO EVENTS **/
  929. /**
  930. * This function is called on a timer. Calls the student's audioChangeCallback
  931. * function and passes it the most recent audio data.
  932. */
  933. CodeHSGraphics.prototype.updateAudio = function() {
  934. analyser.getByteFrequencyData(dataArray);
  935. if (this.audioChangeCallback) {
  936. /* this is the one strange thing. Up above, we set analyser.fftSize. That
  937. * determines how many 'buckets' we split our file into (fft size / 2).
  938. * For some reason, the top 16 'buckets' were always coming out 0, so we
  939. * used .slice() to cut out the last 18 items out of the array. In the
  940. * future, if you want to experiment with different FFT sizes, it will
  941. * be necessary to adjust this slice call (the size of the array will
  942. * definitely change, and number of empty indexes will probably change).
  943. */
  944. var numBuckets = 46;
  945. this.audioChangeCallback(dataArray.slice(0, numBuckets));
  946. }
  947. };
  948. /** KEY EVENTS ****/
  949. window.onkeydown = function(e) {
  950. var index = pressedKeys.indexOf(e.keyCode);
  951. if (index === -1) {
  952. pressedKeys.push(e.keyCode);
  953. }
  954. // Any graphics instance might need to respond to key events.
  955. for (var i = 0; i < allGraphicsInstances.length; i++) {
  956. var curInstance = allGraphicsInstances[i];
  957. if (curInstance.keyDownCallback) {
  958. curInstance.keyDownCallback(e);
  959. }
  960. }
  961. return true;
  962. };
  963. window.onkeyup = function(e) {
  964. var index = pressedKeys.indexOf(e.keyCode);
  965. if (index !== -1) {
  966. pressedKeys.splice(index, 1);
  967. }
  968. // Any graphics instance might need to respond to key events.
  969. for (var i = 0; i < allGraphicsInstances.length; i++) {
  970. var curInstance = allGraphicsInstances[i];
  971. if (curInstance.keyUpCallback) {
  972. curInstance.keyUpCallback(e);
  973. }
  974. }
  975. };
  976. /** RESIZE EVENT ****/
  977. var resizeTimeout;
  978. window.onresize = function(e) {
  979. // https://developer.mozilla.org/en-US/docs/Web/Events/resize
  980. // Throttle the resize event handler since it fires at such a rapid rate
  981. // Only respond to the resize event if there's not already a response queued up
  982. if (!resizeTimeout) {
  983. resizeTimeout = setTimeout(function() {
  984. resizeTimeout = null;
  985. // Any graphics instance might need to respond to resize events.
  986. for (var i = 0; i < allGraphicsInstances.length; i++) {
  987. var curInstance = allGraphicsInstances[i];
  988. if (curInstance.fullscreenMode) {
  989. curInstance.setFullscreen();
  990. }
  991. }
  992. }, DEFAULT_FRAME_RATE);
  993. }
  994. };
  995. /** MOBILE DEVICE EVENTS ****/
  996. if (window.DeviceOrientationEvent) {
  997. window.ondeviceorientation = function(e) {
  998. for (var i = 0; i < allGraphicsInstances.length; i++) {
  999. var curInstance = allGraphicsInstances[i];
  1000. if (curInstance.deviceOrientationCallback) {
  1001. curInstance.deviceOrientationCallback(e);
  1002. }
  1003. }
  1004. };
  1005. }
  1006. if (window.DeviceMotionEvent) {
  1007. window.ondevicemotion = function(e) {
  1008. for (var i = 0; i < allGraphicsInstances.length; i++) {
  1009. var curInstance = allGraphicsInstances[i];
  1010. if (curInstance.deviceMotionCallback) {
  1011. curInstance.deviceMotionCallback(e);
  1012. }
  1013. }
  1014. };
  1015. }
  1016. /* Mouse and Touch Event Helpers */
  1017. // Same for MouseEvent or TouchEvent given the event and target
  1018. // Method based on: http://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-element
  1019. CodeHSGraphics.getBaseCoordinates = function(e, target) {
  1020. var x;
  1021. var y;
  1022. if (e.pageX || e.pageY) {
  1023. x = e.pageX;
  1024. y = e.pageY;
  1025. } else {
  1026. x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
  1027. y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  1028. }
  1029. var offset = target.offset();
  1030. x -= offset.left;
  1031. y -= offset.top;
  1032. return {x: x, y: y};
  1033. };
  1034. CodeHSGraphics.getMouseCoordinates = function(e) {
  1035. var baseCoordinates = CodeHSGraphics.getBaseCoordinates(e, $(e.currentTarget));
  1036. var x = baseCoordinates.x;
  1037. var y = baseCoordinates.y;
  1038. // at zoom levels != 100%, x and y are floats.
  1039. x = Math.round(x);
  1040. y = Math.round(y);
  1041. return {x: x, y: y};
  1042. };
  1043. CodeHSGraphics.getTouchCoordinates = function(e) {
  1044. var baseCoordinates = CodeHSGraphics.getBaseCoordinates(e, $(e.target));
  1045. var x = baseCoordinates.x;
  1046. var y = baseCoordinates.y;
  1047. // canvas almost always gets scaled down for mobile screens, need to figure
  1048. // out the x and y in terms of the unscaled canvas size in pixels otherwise
  1049. // touch coordinates are off
  1050. var screenCanvasWidth = $('#game').width();
  1051. var fullCanvasWidth = $('#game').attr('width');
  1052. var ratio = fullCanvasWidth / screenCanvasWidth;
  1053. x = x * ratio;
  1054. y = y * ratio;
  1055. // at zoom levels != 100%, x and y are floats.
  1056. x = Math.round(x);
  1057. y = Math.round(y);
  1058. return {x: x, y: y};
  1059. };
  1060. MouseEvent.prototype.getX = function() {
  1061. return CodeHSGraphics.getMouseCoordinates(this).x;
  1062. };
  1063. MouseEvent.prototype.getY = function() {
  1064. return CodeHSGraphics.getMouseCoordinates(this).y;
  1065. };
  1066. if (typeof TouchEvent != 'undefined') {
  1067. TouchEvent.prototype.getX = function() {
  1068. return CodeHSGraphics.getTouchCoordinates(this.touches[0]).x;
  1069. };
  1070. TouchEvent.prototype.getY = function() {
  1071. return CodeHSGraphics.getTouchCoordinates(this.touches[0]).y;
  1072. };
  1073. }
  1074. module.exports = {
  1075. CodeHSGraphics: CodeHSGraphics,
  1076. PUBLIC_METHODS: PUBLIC_METHODS,
  1077. PUBLIC_CONSTRUCTORS: PUBLIC_CONSTRUCTORS,
  1078. };