'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,
};