graphics/graphics.js

'use strict';

// Adding Array Methods
Array.prototype.remove = function(idx) {
    return this.splice(idx, 1)[0];
};

// import the npm-hosted editor utils only if the other is not available
var editorUtils = require('codehs-js-utils');

// import graphics utilities functions
var graphicsUtils = require('./graphics-utils.js');

// import audio context utils
var getAudioContext = require('./audioContext.js');

// How often to redraw the display
var DEFAULT_FRAME_RATE = 40;

// Padding between graphics canvas and parent element when in fullscreenMode
var FULLSCREEN_PADDING = 5;

// String list of methods that will be accessible
// to the user
var 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-element

CodeHSGraphics.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,
};