'use strict';
var Thing = require('./thing.js');
var UNDEFINED = -1;
var NOT_LOADED = 0;
var NUM_CHANNELS = 4;
var RED = 0;
var GREEN = 1;
var BLUE = 2;
var ALPHA = 3;
// Keep track of cross origin WebImage URLs that have already been loaded
// so we can take advantage of loading cross origin images from the browser cache
var cachedCrossOriginURLs = {};
/**
* @constructor
* @augments Thing
* @param {string} filename - Filepath to the image
*/
function WebImage(filename) {
if (typeof filename !== 'string') {
throw new TypeError(
'You must pass a string to <span class="code">' +
"new WebImage(filename)</span> that has the image's URL."
);
}
Thing.call(this);
var self = this;
this.image = new Image();
// If the image is from a different origin, we need to request the image using
// crossOrigin 'Anonymous', which allows WebImage to treat the image as
// same origin and manipulate pixel data
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin
var urlParser = document.createElement('a');
urlParser.href = filename;
var src = filename;
if (urlParser.origin != window.location.origin) {
this.image.crossOrigin = 'Anonymous';
// If we've loaded this cross origin URL before, keep using the same URL
if (cachedCrossOriginURLs.hasOwnProperty(filename)) {
src = cachedCrossOriginURLs[filename];
} else {
// Otherwise we need to avoid the browser cache
// Browser may have the image cached without the proper
// Access-Control-Allow-Origin header on the resource
// Ensure that we initiate a new crossOrigin 'anonymous' request for this
// image, rather than pulling from browser cache, by making filename unique
// We'll keep using this unique filename next time
src = filename + '?time=' + Date.now();
cachedCrossOriginURLs[filename] = src;
}
}
this.imageLoaded = false;
this.image.src = src;
this.filename = filename;
this.width = NOT_LOADED;
this.height = NOT_LOADED;
this.image.onload = function() {
self.imageLoaded = true;
self.checkDimensions();
self.loadPixelData();
if (self.loadfn) {
self.loadfn();
}
};
this.set = 0;
this.type = 'WebImage';
this.displayFromData = false;
this.dirtyHiddenCanvas = false;
this.data = NOT_LOADED;
}
WebImage.prototype = new Thing();
WebImage.prototype.constructor = WebImage;
/**
* Set a function to be called when the WebImage is loaded.
*
* @param {function} callback - A function
*/
WebImage.prototype.loaded = function(callback) {
this.loadfn = callback;
};
/**
* Set the image of the WebImage.
*
* @param {string} filename - Filepath to the image
*/
WebImage.prototype.setImage = function(filename) {
if (typeof filename !== 'string') {
throw new TypeError(
'You must pass a string to <span class="code">' +
"new WebImage(filename)</span> that has the image's URL."
);
}
var self = this;
this.image = new Image();
// If the image is from a different origin, we need to request the image using
// crossOrigin 'Anonymous', which allows WebImage to treat the image as
// same origin and manipulate pixel data
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin
var urlParser = document.createElement('a');
urlParser.href = filename;
var src = filename;
if (urlParser.origin != window.location.origin) {
this.image.crossOrigin = 'Anonymous';
// If we've loaded this cross origin URL before, keep using the same URL
if (cachedCrossOriginURLs.hasOwnProperty(filename)) {
src = cachedCrossOriginURLs[filename];
} else {
// Otherwise we need to avoid the browser cache
// Browser may have the image cached without the proper
// Access-Control-Allow-Origin header on the resource
// Ensure that we initiate a new crossOrigin 'anonymous' request for this
// image, rather than pulling from browser cache, by making filename unique
// We'll keep using this unique filename next time
src = filename + '?time=' + Date.now();
cachedCrossOriginURLs[filename] = src;
}
}
this.imageLoaded = false;
this.image.src = src;
this.filename = filename;
this.width = NOT_LOADED;
this.height = NOT_LOADED;
this.image.onload = function() {
self.imageLoaded = true;
self.checkDimensions();
self.loadPixelData();
if (self.loadfn) {
self.loadfn();
}
};
this.set = 0;
this.displayFromData = false;
this.dirtyHiddenCanvas = false;
this.data = NOT_LOADED;
};
/**
* Reinforce the dimensions of the WebImage based on the image it displays.
*/
WebImage.prototype.checkDimensions = function() {
if (this.width == NOT_LOADED && this.imageLoaded) {
this.width = this.image.width;
this.height = this.image.height;
}
};
/**
* Draws the WebImage in the canvas.
*
* @param {CodeHSGraphics} __graphics__ - Instance of the __graphics__ module.
*/
WebImage.prototype.draw = function(__graphics__) {
this.checkDimensions();
var context = __graphics__.getContext('2d');
// Scale and translate
// X scale, X scew, Y scew, Y scale, X position, Y position
context.setTransform(1, 0, 0, 1, this.x + this.width / 2, this.y + this.height / 2);
context.rotate(this.rotation);
// If we should be displaying the underlying pixel data, display that
// Otherwise display the image
var elemToDraw = this.image;
if (this.displayFromData && this.data !== NOT_LOADED && this.hiddenCanvas) {
// Update the in memory canvas with the latest pixel data if necessary
if (this.dirtyHiddenCanvas) {
var ctx = this.hiddenCanvas.getContext('2d');
ctx.clearRect(0, 0, this.hiddenCanvas.width, this.hiddenCanvas.height);
ctx.putImageData(this.data, 0, 0);
this.dirtyHiddenCanvas = false;
}
elemToDraw = this.hiddenCanvas;
}
try {
context.drawImage(elemToDraw, -this.width / 2, -this.height / 2, this.width, this.height);
} catch (err) {
throw new TypeError(
'Unable to create a WebImage from <span class="code">' +
this.filename +
'</span> ' +
'Make sure you have a valid image URL. ' +
'Hint: You can use More > Upload to upload your image and create a valid image URL.'
);
} finally {
// Reset transformation matrix
// X scale, X scew, Y scew, Y scale, X position, Y position
context.setTransform(1, 0, 0, 1, 0, 0);
}
};
/**
* Return the underlying ImageData for this image.
* Read more at https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData
*/
WebImage.prototype.loadPixelData = function() {
if (this.data === NOT_LOADED && this.imageLoaded) {
try {
// get the ImageData for this image
this.hiddenCanvas = document.createElement('canvas');
this.hiddenCanvas.width = this.width;
this.hiddenCanvas.height = this.height;
var ctx = this.hiddenCanvas.getContext('2d');
ctx.drawImage(this.image, 0, 0, this.width, this.height);
this.data = ctx.getImageData(0, 0, this.width, this.height);
this.dirtyHiddenCanvas = false;
} catch (err) {
// NOTE: This should never happen now that we request images using
// image.crossOrigin = 'Anonymous'
// If the image was loaded, that means the external domain gave us CORS
// access to the image and the browser will treat it as if it is same origin,
// meaning we should be allowed to call 'getImageData'
//
// Just in case 'getImageData' fails,
// Fail silently so we can still display the image from cross origin,
// we just don't access the underlying image data
this.data = NOT_LOADED;
}
}
return this.data;
};
/**
* Checks if the passed point is contained in the WebImage.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @returns {boolean} Whether the passed point is contained in the WebImage.
*/
WebImage.prototype.containsPoint = function(x, y) {
return x >= this.x && x <= this.x + this.width && y >= this.y && y <= this.y + this.height;
};
/**
* Gets the width of the WebImage.
*
* @returns {number} Width of the WebImage.
*/
WebImage.prototype.getWidth = function() {
return this.width;
};
/**
* Gets the height of the WebImage.
*
* @returns {number} Height of the WebImage.
*/
WebImage.prototype.getHeight = function() {
return this.height;
};
/**
* Sets the size of the WebImage.
*
* @param {number} width - The desired width of the resulting WebImage.
* @param {number} height - The desired height of the resulting WebImage.
*/
WebImage.prototype.setSize = function(width, height) {
if (arguments.length !== 2) {
throw new Error(
'You should pass exactly 2 arguments to <span ' +
'class="code">setSize(width, height)</span>'
);
}
if (typeof width !== 'number' || !isFinite(width)) {
throw new TypeError(
'Invalid value for <span class="code">width' +
'</span>. Make sure you are passing finite numbers to <span ' +
'class="code">setSize(width, height)</span>. Did you ' +
'forget the parentheses in <span class="code">getWidth()</span> ' +
'or <span class="code">getHeight()</span>? Or did you perform a ' +
'calculation on a variable that is not a number?'
);
}
if (typeof height !== 'number' || !isFinite(height)) {
throw new TypeError(
'Invalid value for <span class="code">height' +
'</span>. Make sure you are passing finite numbers to <span ' +
'class="code">setSize(width, height)</span>. Did you ' +
'forget the parentheses in <span class="code">getWidth()</span> ' +
'or <span class="code">getHeight()</span>? Or did you perform a ' +
'calculation on a variable that is not a number?'
);
}
this.width = Math.max(0, width);
this.height = Math.max(0, height);
};
/* Get and set pixel functions */
/**
* Gets a pixel at the given x and y coordinates.
* Read more here:
* https://developer.mozilla.org/en-US/docs/Web/API/ImageData/data
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @returns {array} An array of 4 numbers representing the (r,g,b,a) values
* of the pixel at that coordinate.
*/
WebImage.prototype.getPixel = function(x, y) {
if (this.data === NOT_LOADED || x > this.width || x < 0 || y > this.height || y < 0) {
var noPixel = [UNDEFINED, UNDEFINED, UNDEFINED, UNDEFINED];
return noPixel;
} else {
var index = NUM_CHANNELS * (y * this.width + x);
var pixel = [
this.data.data[index + RED],
this.data.data[index + GREEN],
this.data.data[index + BLUE],
this.data.data[index + ALPHA],
];
return pixel;
}
};
/**
* Get the red value at a given location in the image.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @returns {integer} An integer between 0 and 255.
*/
WebImage.prototype.getRed = function(x, y) {
return this.getPixel(x, y)[RED];
};
/**
* Get the green value at a given location in the image.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @returns {integer} An integer between 0 and 255.
*/
WebImage.prototype.getGreen = function(x, y) {
return this.getPixel(x, y)[GREEN];
};
/**
* Get the blue value at a given location in the image.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @returns {integer} An integer between 0 and 255.
*/
WebImage.prototype.getBlue = function(x, y) {
return this.getPixel(x, y)[BLUE];
};
/**
* Get the alpha value at a given location in the image.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @returns {integer} An integer between 0 and 255.
*/
WebImage.prototype.getAlpha = function(x, y) {
return this.getPixel(x, y)[ALPHA];
};
/**
* Set the `component` value at a given location in the image to `val`.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @param {integer} component - Integer representing the color value to
* be set. R, G, B = 0, 1, 2, respectively.
* @param {integer} val - The desired value of the `component` at the pixel.
* Must be between 0 and 255.
*/
WebImage.prototype.setPixel = function(x, y, component, val) {
if (this.data !== NOT_LOADED && !(x < 0 || y < 0 || x > this.width || y > this.height)) {
// Update the pixel value
var index = NUM_CHANNELS * (y * this.width + x);
this.data.data[index + component] = val;
// Now that we have modified the image data, we need to display
// the image based on the underlying image data rather than the
// image url
this.displayFromData = true;
this.dirtyHiddenCanvas = true;
}
};
/**
* Set the red value at a given location in the image to `val`.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @param {integer} val - The desired value of the red component at the pixel.
* Must be between 0 and 255.
*/
WebImage.prototype.setRed = function(x, y, val) {
this.setPixel(x, y, RED, val);
};
/**
* Set the green value at a given location in the image to `val`.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @param {integer} val - The desired value of the green component at the pixel.
* Must be between 0 and 255.
*/
WebImage.prototype.setGreen = function(x, y, val) {
this.setPixel(x, y, GREEN, val);
};
/**
* Set the blue value at a given location in the image to `val`.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @param {integer} val - The desired value of the blue component at the pixel.
* Must be between 0 and 255.
*/
WebImage.prototype.setBlue = function(x, y, val) {
this.setPixel(x, y, BLUE, val);
};
/**
* Set the alpha value at a given location in the image to `val`.
*
* @param {number} x - The x coordinate of the point being tested.
* @param {number} y - The y coordinate of the point being tested.
* @param {integer} val - The desired value of the alpha component at the
* pixel.
* Must be between 0 and 255.
*/
WebImage.prototype.setAlpha = function(x, y, val) {
this.setPixel(x, y, ALPHA, val);
};
module.exports = WebImage;