'use strict';// Adding Array MethodsArray.prototype.remove = function(idx) { return this.splice(idx, 1)[0];};// import the npm-hosted editor utils only if the other is not availablevar editorUtils = require('codehs-js-utils');// import graphics utilities functionsvar graphicsUtils = require('./graphics-utils.js');// import audio context utilsvar getAudioContext = require('./audioContext.js');// How often to redraw the displayvar DEFAULT_FRAME_RATE = 40;// Padding between graphics canvas and parent element when in fullscreenModevar FULLSCREEN_PADDING = 5;// String list of methods that will be accessible// to the uservar PUBLIC_METHODS = [];var PUBLIC_CONSTRUCTORS = [];// Pressed keys are actually maintained acorss all// graphics instances since there is only one keyboard.var pressedKeys = [];// Keep track of all graphics instances.var allGraphicsInstances = [];var graphicsInstanceId = 0;var analyser;var dataArray;var gainNode;var source;var audioCtx = 0;/** * Set up an instance of the graphics library. * @constructor * @param {dictionary} options - Options, primarily .canvas, the selector * string for the canvas. * If multiple are returned, we'll take the first one. * If none is passed, we'll look for any canvas * tag on the page. */function CodeHSGraphics(options) { options = options || {}; this.resetAllState(); this.globalTimer = true; this.currentCanvas = null; this.setCurrentCanvas(options.canvas); // Are we in debug mode? The default is false. this.debugMode = options.debug || false; this.fullscreenMode = false; // Since we now have multiple instances of the graphics object // give each one a unique id this.instanceId = graphicsInstanceId; graphicsInstanceId++; // override any graphics instance that is already using this ID. // if there aren't any, just push this instance onto the end. var existingId = this.canvasHasInstance(options.canvas); if (existingId !== null) { var existingGraphics = allGraphicsInstances[existingId]; existingGraphics.stopTimer('MAIN_TIMER'); allGraphicsInstances[existingId] = this; } else { allGraphicsInstances.push(this); }}/** * Adds a method to the public methods constant. * @param {string} name - Name of the method. */CodeHSGraphics.registerPublicMethod = function(name) { PUBLIC_METHODS.push(name);};/** * Adds a constructor to the public constructors constant. * @param {string} name - Name of the object to be constructed. */CodeHSGraphics.registerConstructorMethod = function(name) { PUBLIC_CONSTRUCTORS.push(name);};/** * Generate strings for the public methods to bring them to the * public namespace without having to call them with the graphics instance. * @returns {string} Line broken function definitions. */CodeHSGraphics.getNamespaceModifcationString = function() { var result = '\n'; for (var i = 0; i < PUBLIC_METHODS.length; i++) { var curMethod = PUBLIC_METHODS[i]; // Actually create a method in this scope with the name of the // method so the student can easily access it. For example, we // might have a method like CodeHSGraphics.prototype.add, but we // want the student to be able to access it with just `add`, but // the proper context for this. result += 'function ' + curMethod + '(){\n' + '\treturn __graphics__.' + curMethod + '.apply(__graphics__, arguments);\n' + '}\n'; // result += 'var ' + curMethod + ' = __graphics__.' + curMethod + ';\n'; } return result;};/** * Generate strings for the public constructors to bring them to the * public namespace without having to call them with the graphics instance. * @returns {string} Line broken constructor declarations. */CodeHSGraphics.getConstructorModificationString = function() { var result = ''; for (var i = 0; i < PUBLIC_CONSTRUCTORS.length; i++) { var curMethod = PUBLIC_CONSTRUCTORS[i]; result += 'var ' + curMethod + ' = __graphics__.' + curMethod + ';\n'; } return result;};/** ************* PUBLIC METHODS *******************/// NOTE: if you add a public method, you MUST fix linenumber calc for errors:// function getCorrectLineNumber in editorErrors.js// adding a public method will add 3 lines to the program./** * Add an element to the graphics instance. * @param {Thing} elem - A subclass of Thing to be added to the graphics instance. */CodeHSGraphics.prototype.add = function(elem) { this.elements.push(elem);};CodeHSGraphics.registerPublicMethod('add');/** * Wrapper around Audio so we have reference to all Audio objects created. * @param{String} url - url of the audio file. */window.oldAudio = window.Audio;CodeHSGraphics.prototype.Audio = function(url) { var audioElem = new oldAudio(url); audioElem.crossOrigin = 'anonymous'; this.audioElements.push(audioElem); return audioElem;};CodeHSGraphics.prototype.Audio.constructor = window.oldAudio;CodeHSGraphics.registerPublicMethod('Audio');/** * Wrapper around Sound so we have reference to all Sound objects created. * Following the example set by tracking Audio elements. * @param frequency - Either a number (Hertz) or note ("C#4" for middle C Sharp) * @param oscillatorType {string} - several options * basic types: "sine", "triangle", "square", "sawtooth" * any basic type can be prefixed with "fat", "am" or "fm", ie "fatsawtooth" * any basic type can be suffixed with a number ie "4" for the number of partials * ie "square4" * special types: "pwm", "pulse" * drum instrument: "membrane" * cymbal instrument: "metal" * https://tonejs.github.io/docs/13.8.25/OmniOscillator */var oldSound = require('./sound.js');CodeHSGraphics.prototype.Sound = function(frequency, oscillatorType) { frequency = frequency || 440; oscillatorType = oscillatorType || 'fatsawtooth'; var soundElem = new oldSound(frequency, oscillatorType); this.soundElements.push(soundElem); return soundElem;};CodeHSGraphics.prototype.Sound.constructor = oldSound;CodeHSGraphics.registerPublicMethod('Sound');/** * Record a click. */CodeHSGraphics.prototype.waitForClick = function() { this.clickCount++;};CodeHSGraphics.registerPublicMethod('waitForClick');/** * Assign a function as a callback for click (mouse down, mouse up) events. * @param {function} fn - A callback to be triggered on click events. */CodeHSGraphics.prototype.mouseClickMethod = function(fn) { this.clickCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('mouseClickMethod');/** * Assign a function as a callback for mouse move events. * @param {function} fn - A callback to be triggered on mouse move events. */CodeHSGraphics.prototype.mouseMoveMethod = function(fn) { this.moveCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('mouseMoveMethod');/** * Assign a function as a callback for mouse down events. * @param {function} fn - A callback to be triggered on mouse down. */CodeHSGraphics.prototype.mouseDownMethod = function(fn) { this.mouseDownCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('mouseDownMethod');/** * Assign a function as a callback for mouse up events. * @param {function} fn - A callback to be triggered on mouse up events. */CodeHSGraphics.prototype.mouseUpMethod = function(fn) { this.mouseUpCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('mouseUpMethod');/** * Assign a function as a callback for drag events. * @param {function} fn - A callback to be triggered on drag events. */CodeHSGraphics.prototype.mouseDragMethod = function(fn) { this.dragCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('mouseDragMethod');/** * Assign a function as a callback for keydown events. * @param {function} fn - A callback to be triggered on keydown events. */CodeHSGraphics.prototype.keyDownMethod = function(fn) { this.keyDownCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('keyDownMethod');/** * Assign a function as a callback for key up events. * @param {function} fn - A callback to be triggered on key up events. */CodeHSGraphics.prototype.keyUpMethod = function(fn) { this.keyUpCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('keyUpMethod');/** * Assign a function as a callback for device orientation events. * @param {function} fn - A callback to be triggered on device orientation * events. */CodeHSGraphics.prototype.deviceOrientationMethod = function(fn) { this.deviceOrientationCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('deviceOrientationMethod');/** * Assign a function as a callback for device motion events. * @param {function} fn - A callback to be triggered device motion events. */CodeHSGraphics.prototype.deviceMotionMethod = function(fn) { this.deviceMotionCallback = editorUtils.safeCallback(fn);};CodeHSGraphics.registerPublicMethod('deviceMotionMethod');/** * Assign a function as a callback for when audio data changes for audio * being played in a graphics program. * @param {object} tag - Audio element playing sound to analyze * @param {function} fn - A callback to be triggered on audio data change. */CodeHSGraphics.prototype.audioChangeMethod = function(tag, fn) { // get new audio context and create analyser audioCtx = getAudioContext(); // IE browser exit gracefully if (!audioCtx) { return; } analyser = audioCtx.createAnalyser(); // set fft -- used to set the number of slices we break our frequency range // in to. analyser.fftSize = 128; // gt bugger length and create a new array in that size var bufferLength = analyser.frequencyBinCount; dataArray = new Uint8Array(bufferLength); // create media source from student's audio tag source = audioCtx.createMediaElementSource(tag); // should allow cors source.crossOrigin = 'anonymous'; // connect analyzer to sound source.connect(analyser); // create gain node and connect to sound (makes speaker output possuble) var gainNode = audioCtx.createGain(); source.connect(gainNode); gainNode.connect(audioCtx.destination); // create callback fn and assign attach to timer this.audioChangeCallback = editorUtils.safeCallback(fn); this.setGraphicsTimer(this.updateAudio.bind(this), DEFAULT_FRAME_RATE, null, 'updateAudio');};CodeHSGraphics.registerPublicMethod('audioChangeMethod');/** * Check if a key is currently pressed * @param {integer} keyCode - Key code of key being checked. * @returns {boolean} Whether or not that key is being pressed. */CodeHSGraphics.prototype.isKeyPressed = function(keyCode) { return pressedKeys.indexOf(keyCode) != -1;};CodeHSGraphics.registerPublicMethod('isKeyPressed');/** * Get the width of the entire graphics canvas. * @returns {float} The width of the canvas. */CodeHSGraphics.prototype.getWidth = function() { var canvas = this.getCanvas(); return parseFloat(canvas.getAttribute('width'));};CodeHSGraphics.registerPublicMethod('getWidth');/** * Get the height of the entire graphics canvas. * @returns {float} The height of the canvas. */CodeHSGraphics.prototype.getHeight = function() { var canvas = this.getCanvas(); return parseFloat(canvas.getAttribute('height'));};CodeHSGraphics.registerPublicMethod('getHeight');/** * Remove a timer associated with a function. * @param {function} fn - Function whose timer is removed. * note 'fn' may also be the name of the function. */CodeHSGraphics.prototype.stopTimer = function(fn) { var key = typeof fn === 'function' ? fn.name : fn; clearInterval(this.timers[key]);};CodeHSGraphics.registerPublicMethod('stopTimer');/** * Stop all timers. */CodeHSGraphics.prototype.stopAllTimers = function() { for (var i = 1; i < 99999; i++) { window.clearInterval(i); } this.setMainTimer();};CodeHSGraphics.registerPublicMethod('stopAllTimers');/** * Create a new timer * @param {function} fn - Function to be called at intervals. * @param {integer} time - Time interval to call function `fn` * @param {dictionary} data - Any data associated with the timer. * @param {string} name - Name of this timer. */CodeHSGraphics.prototype.setTimer = function(fn, time, data, name) { if (arguments.length < 2) { throw new Error( '2 parameters required for <span class="code">' + 'setTimer</span>, ' + arguments.length + ' found. You must ' + 'provide a callback function and ' + 'a number representing the time delay ' + 'to <span class="code">setTimer</span>' ); } if (typeof fn !== 'function') { throw new TypeError( 'Invalid callback function. ' + 'Make sure you are passing an actual function to ' + '<span class="code">setTimer</span>.' ); } if (typeof time !== 'number' || !isFinite(time)) { throw new TypeError( 'Invalid value for time delay. ' + 'Make sure you are passing a finite number to ' + '<span class="code">setTimer</span> for the delay.' ); } var self = this; // Safety, set a min frequency if (isNaN(time) || time < 15) { time = 15; } if (this.waitingForClick()) { this.delayedTimers.push({ fn: fn, time: time, data: data, clicks: self.clickCount, name: name, }); } else { this.setGraphicsTimer(fn, time, data, name); }};CodeHSGraphics.registerPublicMethod('setTimer');/** * Set the background color of the canvas. * @param {Color} color - The desired color of the canvas. */CodeHSGraphics.prototype.setBackgroundColor = function(color) { this.backgroundColor = color;};CodeHSGraphics.registerPublicMethod('setBackgroundColor');/** * Clear everything from the canvas. */CodeHSGraphics.prototype.clear = function(context) { var ctx = context || this.getContext(); ctx.clearRect(0, 0, this.getWidth(), this.getHeight());};CodeHSGraphics.registerPublicMethod('clear');/** * Get an element at a specific point. * If several elements are present at the position, return the one put there first. * @param {number} x - The x coordinate of a point to get element at. * @param {number} y - The y coordinate of a point to get element at. * @returns {Thing|null} The object at the point (x, y), if there is one (else null). */CodeHSGraphics.prototype.getElementAt = function(x, y) { for (var i = this.elements.length - 1; i >= 0; i--) { if (this.elements[i].containsPoint(x, y, this)) { return this.elements[i]; } } return null;};CodeHSGraphics.registerPublicMethod('getElementAt');/** * Check if an element exists with the given paramenters. * @param {object} params - Dictionary of parameters for the object. * Includes x, y, heigh, width, color, radius, label and type. * @returns {boolean} */CodeHSGraphics.prototype.elementExistsWithParameters = function(params) { for (var i = this.elements.length - 1; i >= 0; i--) { var elem = this.elements[i]; try { if ( params.x !== undefined && this.runCode('return ' + params.x).result.toFixed(0) != elem.getX().toFixed(0) ) { continue; } if ( params.y !== undefined && this.runCode('return ' + params.y).result.toFixed(0) != elem.getY().toFixed(0) ) { continue; } if ( params.width !== undefined && this.runCode('return ' + params.width).result.toFixed(0) != elem.getWidth().toFixed(0) ) { continue; } if ( params.height !== undefined && this.runCode('return ' + params.height).result.toFixed(0) != elem.getHeight().toFixed(0) ) { continue; } if ( params.radius !== undefined && this.runCode('return ' + params.radius).result.toFixed(0) != elem.getRadius().toFixed(0) ) { continue; } if ( params.color !== undefined && this.runCode('return ' + params.color).result != elem.getColor() ) { continue; } if (params.label !== undefined && params.label != elem.getLabel()) { continue; } if (params.type !== undefined && params.type != elem.getType()) { continue; } } catch (err) { continue; } return true; } return false;};CodeHSGraphics.registerPublicMethod('elementExistsWithParameters');/** * Remove all elements from the canvas. */CodeHSGraphics.prototype.removeAll = function() { this.stopAllVideo(); this.elements = [];};CodeHSGraphics.registerPublicMethod('removeAll');/** * Remove a specific element from the canvas. * @param {Thing} elem - The element to be removed from the canvas. */CodeHSGraphics.prototype.remove = function(elem) { for (var i = 0; i < this.elements.length; i++) { if (this.elements[i] == elem) { if (this.elements[i].type == 'WebVideo') { this.elements[i].stop(); } this.elements.splice(i, 1); // Remove from list } }};CodeHSGraphics.registerPublicMethod('remove');/** * Set the size of the canvas. * @param {number} w - Desired width of the canvas. * @param {number} h - Desired height of the canvas. */CodeHSGraphics.prototype.setSize = function(w, h) { this.fullscreenMode = false; var canvas = this.getCanvas(); canvas.width = w; canvas.height = h; $(canvas).css({ 'max-height': h, 'max-width': w, });};CodeHSGraphics.registerPublicMethod('setSize');/** * Set the canvas to take up the entire parent element */CodeHSGraphics.prototype.setFullscreen = function() { var self = this; self.fullscreenMode = true; // when this is true, canvas will resize with parent var canvas = this.getCanvas(); canvas.width = canvas.parentElement.offsetWidth - FULLSCREEN_PADDING; canvas.height = canvas.parentElement.offsetHeight - FULLSCREEN_PADDING; $(canvas).css({ 'max-height': canvas.height, 'max-width': canvas.width, });};CodeHSGraphics.registerPublicMethod('setFullscreen');/** **************** SHAPE CONSTRUCTORS **************/// Insertion point for graphics modules.CodeHSGraphics.prototype.Rectangle = require('./rectangle.js');CodeHSGraphics.registerConstructorMethod('Rectangle');CodeHSGraphics.prototype.Circle = require('./circle.js');CodeHSGraphics.registerConstructorMethod('Circle');CodeHSGraphics.prototype.Line = require('./line.js');CodeHSGraphics.registerConstructorMethod('Line');CodeHSGraphics.prototype.Grid = require('./grid.js');CodeHSGraphics.registerConstructorMethod('Grid');CodeHSGraphics.prototype.Line = require('./line.js');CodeHSGraphics.registerConstructorMethod('Line');CodeHSGraphics.prototype.Polygon = require('./polygon.js');CodeHSGraphics.registerConstructorMethod('Polygon');CodeHSGraphics.prototype.Text = require('./text.js');CodeHSGraphics.registerConstructorMethod('Text');CodeHSGraphics.prototype.Oval = require('./oval.js');CodeHSGraphics.registerConstructorMethod('Oval');CodeHSGraphics.prototype.Arc = require('./arc.js');CodeHSGraphics.registerConstructorMethod('Arc');CodeHSGraphics.prototype.Color = require('./color.js');CodeHSGraphics.registerConstructorMethod('Color');CodeHSGraphics.prototype.WebImage = require('./webimage.js');CodeHSGraphics.registerConstructorMethod('WebImage');CodeHSGraphics.prototype.WebVideo = require('./webvideo.js');CodeHSGraphics.registerConstructorMethod('WebVideo');CodeHSGraphics.prototype.ImageLibrary = require('./imagelibrary.js');CodeHSGraphics.registerConstructorMethod('ImageLibrary');/** **************** PRIVATE METHODS *****************//** * This is how you run the code, but get access to the * state of the graphics library. The current instance * becomes accessible in the code. * @param {string} code - The code from the editor. */CodeHSGraphics.prototype.runCode = function(code, options) { options = options || {}; var getPublicMethodString = CodeHSGraphics.getNamespaceModifcationString(); var getConstructorModificationString = CodeHSGraphics.getConstructorModificationString(); var wrap = ''; // Give the user easy access to public graphics methods // in the proper context. wrap += getPublicMethodString; wrap += getConstructorModificationString; // Set up `Text` so we don't need to redefine it on the window wrap += ';var __nativeText=window.Text;var Text=__graphics__.Text;'; // Text objects need access to some 2d graphics context to compute // height and width. This might be done before a draw call. wrap += '\nText.giveDefaultContext(__graphics__);\n'; // Set up `Set` wrap += ';var __nativeSet=window.Set;var Set=window.chsSet;'; if (!options.overrideInfiniteLoops) { // tool all while loops var whileLoopRegEx = /while\s*\((.*)\)\s*{/gm; var forLoopRegEx = /for\s*\((.*)\)\s*{/gm; var doWhileRegEx = /do\s*\{/gm; // Inject into while loops code = code.replace(whileLoopRegEx, function(match, p1, offset, string) { var lineNumber = string.slice(0, offset).split('\n').length; var c = "if(___nloops++>15000){var e = new Error('Your while loop on line " + lineNumber + " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " + lineNumber + '; throw e;}'; return 'var ___nloops=0;while(' + p1 + ') {' + c; }); // Inject into for loops code = code.replace(forLoopRegEx, function(match, p1, offset, string) { var lineNumber = string.slice(0, offset).split('\n').length; var c = "if(___nloops++>15000){var e = new Error('Your for loop on line " + lineNumber + " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " + lineNumber + '; throw e;}'; return 'var ___nloops=0;for(' + p1 + '){' + c; }); // Inject into do-while loops code = code.replace(doWhileRegEx, function(match, offset, string) { var lineNumber = string.slice(0, offset).split('\n').length; var c = "if(___nloops++>15000){var e = new Error('Your do-while loop on line " + lineNumber + " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " + lineNumber + '; throw e;}'; return 'var ___nloops=0;do {' + c; }); } // User code. wrap += code; // Call the start function wrap += "\n\nif(typeof start == 'function') {start();} "; wrap += ';window.Text=__nativeText;'; wrap += ';window.Set=__nativeSet;'; return editorUtils.safeEval(wrap, this, '__graphics__');};/** * Resets all the timers to time 0. */CodeHSGraphics.prototype.resetAllTimers = function() { for (var cur in this.timers) { clearInterval(this.timers[cur]); }};CodeHSGraphics.prototype.stopAllAudio = function() { this.audioElements.forEach(function(audio) { audio.pause(); }); this.soundElements.forEach(function(soundElem) { soundElem.stop(); soundElem.disconnect(); });};CodeHSGraphics.prototype.stopAllVideo = function() { for (var i = 0; i < this.elements.length; i++) { if (this.elements[i].type == 'WebVideo') { this.elements[i].stop(); } }};/** * Resets the graphics instance to a clean slate. */CodeHSGraphics.prototype.resetAllState = function() { this.backgroundColor = null; this.elements = []; this.audioElements = []; this.soundElements = []; this.clickCallback = null; this.moveCallback = null; this.mouseDownCallback = null; this.mouseUpCallback = null; this.dragCallback = null; this.keyDownCallback = null; this.keyUpCallback = null; this.deviceOrientationCallback = null; this.deviceMotionCallback = null; this.audioChangeCallback = null; // if audio source exists, disconnect it if (source) { source.disconnect(); source = 0; } // A fast hash from timer key to timer interval # this.timers = {}; // A useful list to store information about all timers. this.timersList = []; this.clickCount = 0; this.delayedTimers = []; // if audio context exists, close it and reset audioCtx if (audioCtx) { audioCtx.close(); audioCtx = 0; } this.fullscreenMode = false;};/** * Reset all timers to 0 and clear timers and canvas. */CodeHSGraphics.prototype.fullReset = function() { this.stopAllAudio(); this.stopAllVideo(); this.resetAllTimers(); this.resetAllState(); /* THIS LINE OF CODE. Leave it commented out. * If we override this setting ( like we do in karel) * it shouldn't be reset to true. */ // this.globalTimer = true; this.setMainTimer();};/** * Return if the graphics canvas exists. * @returns {boolean} Whether or not the canvas exists. */CodeHSGraphics.prototype.canvasExists = function() { return this.getCanvas() !== null;};/** * Return the current canvas we are using. If there is no * canvas on the page this will return null. * @returns {object} The current canvas. */CodeHSGraphics.prototype.getCanvas = function() { return this.currentCanvas;};/** * Set the current canvas we are working with. If no canvas * tag matches the selectorv then we will just have the current * canvas set to null. * @param {string} canvasSelector - String representing canvas class or ID. * Selected with jQuery. */CodeHSGraphics.prototype.setCurrentCanvas = function(canvasSelector) { /* If we were passed a selector, get the first matching * element. */ if (canvasSelector) { this.currentCanvas = $(canvasSelector)[0]; } else { this.currentCanvas = document.getElementsByTagName('canvas')[0]; } // If it is a falsey value like undefined, set it to null. if (!this.currentCanvas) { this.currentCanvas = null; } // On changing the canvas reset the state. this.fullReset(); this.setup();};/** * Stop the global timer */CodeHSGraphics.prototype.stopGlobalTimer = function() { this.globalTimer = false;};/** * Draw the background color for the current object. */CodeHSGraphics.prototype.drawBackground = function() { if (this.backgroundColor) { var context = this.getContext(); context.fillStyle = this.backgroundColor; context.beginPath(); context.rect(0, 0, this.getWidth(), this.getHeight()); context.closePath(); context.fill(); }};/** * Return the 2D graphics context for this graphics * object, or null if none exists. * @returns {context} The 2D graphics context. */CodeHSGraphics.prototype.getContext = function() { var drawingCanvas = this.getCanvas(); // Check the element is in the DOM and the browser supports canvas if (drawingCanvas && drawingCanvas.getContext) { // Initaliase a 2-dimensional drawing context var context = drawingCanvas.getContext('2d'); return context; } return null;};/** * Redraw this graphics canvas. */CodeHSGraphics.prototype.redraw = function() { this.clear(); this.drawBackground(); for (var i = 0; i < this.elements.length; i++) { this.elements[i].draw(this); }};/** * Set the main timer for graphics. */CodeHSGraphics.prototype.setMainTimer = function() { var self = this; /* Refresh the screen every 40 ms */ if (this.globalTimer) { this.setTimer( function() { self.redraw(); }, DEFAULT_FRAME_RATE, null, 'MAIN_TIMER' ); }};/** * Whether the graphics instance is waiting for a click. * @returns {boolean} Whether or not the instance is waiting for a click. */CodeHSGraphics.prototype.waitingForClick = function() { return this.clickCount !== 0;};/** * Whether the selected canvas already has an instance associated. */CodeHSGraphics.prototype.canvasHasInstance = function(canvas) { var instance; for (var i = 0; i < allGraphicsInstances.length; i++) { instance = allGraphicsInstances[i]; if (instance.instanceId !== this.instanceId && instance.getCanvas() === canvas) { return instance.instanceId; } } return null;};/** * Get the distance between two points, (x1, y1) and (x2, y2) * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @returns {number} Distance between the two points. */CodeHSGraphics.prototype.getDistance = function(x1, y1, x2, y2) { return graphicsUtils.getDistance(x1, y1, x2, y2);};/** * Set up the graphics instance to prepare for interaction */CodeHSGraphics.prototype.setup = function() { var self = this; var drawingCanvas = this.getCanvas(); // self.setMainTimer(); drawingCanvas.onclick = function(e) { if (self.waitingForClick()) { self.clickCount--; for (var i = 0; i < self.delayedTimers.length; i++) { var timer = self.delayedTimers[i]; timer.clicks--; if (timer.clicks === 0) { self.setGraphicsTimer(timer.fn, timer.time, timer.data); } } return; } if (self.clickCallback) { self.clickCallback(e); } }; var mouseDown = false; drawingCanvas.onmousemove = function(e) { if (self.moveCallback) { self.moveCallback(e); } if (mouseDown && self.dragCallback) { self.dragCallback(e); } }; drawingCanvas.onmousedown = function(e) { mouseDown = true; if (self.mouseDownCallback) { self.mouseDownCallback(e); } }; drawingCanvas.onmouseup = function(e) { mouseDown = false; if (self.mouseUpCallback) { self.mouseUpCallback(e); } }; // TOUCH EVENTS! drawingCanvas.ontouchmove = function(e) { e.preventDefault(); if (self.dragCallback) { self.dragCallback(e); } else if (self.moveCallback) { self.moveCallback(e); } }; drawingCanvas.ontouchstart = function(e) { e.preventDefault(); if (self.mouseDownCallback) { self.mouseDownCallback(e); } else if (self.clickCallback) { self.clickCallback(e); } if (self.waitingForClick()) { self.clickCount--; for (var i = 0; i < self.delayedTimers.length; i++) { var timer = self.delayedTimers[i]; timer.clicks--; if (timer.clicks === 0) { self.setGraphicsTimer(timer.fn, timer.time, timer.data); } } return; } }; drawingCanvas.ontouchend = function(e) { e.preventDefault(); if (self.mouseUpCallback) { self.mouseUpCallback(e); } };};/** * Set a graphics timer. * @param {function} fn - The function to be executed on the timer. * @param {number} time - The time interval for the function. * @param {object} data - Any arguments to be passed into `fn`. * @param {string} name - The name of the timer. */CodeHSGraphics.prototype.setGraphicsTimer = function(fn, time, data, name) { if (typeof name === 'undefined') { name = fn.name; } this.timers[name] = editorUtils.safeSetInterval(fn, data, time); this.timersList.push({ name: name, fn: fn, data: data, time: time, });};/** AUDIO EVENTS **//** * This function is called on a timer. Calls the student's audioChangeCallback * function and passes it the most recent audio data. */CodeHSGraphics.prototype.updateAudio = function() { analyser.getByteFrequencyData(dataArray); if (this.audioChangeCallback) { /* this is the one strange thing. Up above, we set analyser.fftSize. That * determines how many 'buckets' we split our file into (fft size / 2). * For some reason, the top 16 'buckets' were always coming out 0, so we * used .slice() to cut out the last 18 items out of the array. In the * future, if you want to experiment with different FFT sizes, it will * be necessary to adjust this slice call (the size of the array will * definitely change, and number of empty indexes will probably change). */ var numBuckets = 46; this.audioChangeCallback(dataArray.slice(0, numBuckets)); }};/** KEY EVENTS ****/window.onkeydown = function(e) { var index = pressedKeys.indexOf(e.keyCode); if (index === -1) { pressedKeys.push(e.keyCode); } // Any graphics instance might need to respond to key events. for (var i = 0; i < allGraphicsInstances.length; i++) { var curInstance = allGraphicsInstances[i]; if (curInstance.keyDownCallback) { curInstance.keyDownCallback(e); } } return true;};window.onkeyup = function(e) { var index = pressedKeys.indexOf(e.keyCode); if (index !== -1) { pressedKeys.splice(index, 1); } // Any graphics instance might need to respond to key events. for (var i = 0; i < allGraphicsInstances.length; i++) { var curInstance = allGraphicsInstances[i]; if (curInstance.keyUpCallback) { curInstance.keyUpCallback(e); } }};/** RESIZE EVENT ****/var resizeTimeout;window.onresize = function(e) { // https://developer.mozilla.org/en-US/docs/Web/Events/resize // Throttle the resize event handler since it fires at such a rapid rate // Only respond to the resize event if there's not already a response queued up if (!resizeTimeout) { resizeTimeout = setTimeout(function() { resizeTimeout = null; // Any graphics instance might need to respond to resize events. for (var i = 0; i < allGraphicsInstances.length; i++) { var curInstance = allGraphicsInstances[i]; if (curInstance.fullscreenMode) { curInstance.setFullscreen(); } } }, DEFAULT_FRAME_RATE); }};/** MOBILE DEVICE EVENTS ****/if (window.DeviceOrientationEvent) { window.ondeviceorientation = function(e) { for (var i = 0; i < allGraphicsInstances.length; i++) { var curInstance = allGraphicsInstances[i]; if (curInstance.deviceOrientationCallback) { curInstance.deviceOrientationCallback(e); } } };}if (window.DeviceMotionEvent) { window.ondevicemotion = function(e) { for (var i = 0; i < allGraphicsInstances.length; i++) { var curInstance = allGraphicsInstances[i]; if (curInstance.deviceMotionCallback) { curInstance.deviceMotionCallback(e); } } };}/* Mouse and Touch Event Helpers */// Same for MouseEvent or TouchEvent given the event and target// Method based on: http://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-elementCodeHSGraphics.getBaseCoordinates = function(e, target) { var x; var y; if (e.pageX || e.pageY) { x = e.pageX; y = e.pageY; } else { x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; } var offset = target.offset(); x -= offset.left; y -= offset.top; return {x: x, y: y};};CodeHSGraphics.getMouseCoordinates = function(e) { var baseCoordinates = CodeHSGraphics.getBaseCoordinates(e, $(e.currentTarget)); var x = baseCoordinates.x; var y = baseCoordinates.y; // at zoom levels != 100%, x and y are floats. x = Math.round(x); y = Math.round(y); return {x: x, y: y};};CodeHSGraphics.getTouchCoordinates = function(e) { var baseCoordinates = CodeHSGraphics.getBaseCoordinates(e, $(e.target)); var x = baseCoordinates.x; var y = baseCoordinates.y; // canvas almost always gets scaled down for mobile screens, need to figure // out the x and y in terms of the unscaled canvas size in pixels otherwise // touch coordinates are off var screenCanvasWidth = $('#game').width(); var fullCanvasWidth = $('#game').attr('width'); var ratio = fullCanvasWidth / screenCanvasWidth; x = x * ratio; y = y * ratio; // at zoom levels != 100%, x and y are floats. x = Math.round(x); y = Math.round(y); return {x: x, y: y};};MouseEvent.prototype.getX = function() { return CodeHSGraphics.getMouseCoordinates(this).x;};MouseEvent.prototype.getY = function() { return CodeHSGraphics.getMouseCoordinates(this).y;};if (typeof TouchEvent != 'undefined') { TouchEvent.prototype.getX = function() { return CodeHSGraphics.getTouchCoordinates(this.touches[0]).x; }; TouchEvent.prototype.getY = function() { return CodeHSGraphics.getTouchCoordinates(this.touches[0]).y; };}module.exports = { CodeHSGraphics: CodeHSGraphics, PUBLIC_METHODS: PUBLIC_METHODS, PUBLIC_CONSTRUCTORS: PUBLIC_CONSTRUCTORS,};