/** * Associates a canvas to a given image, containing a number of renderings * of the image at various sizes. * * This technique is known as 'mipmapping'. * * NOTE: Images can also be of type 'data:svg+xml`. This code also works * for svg, but the mipmapping may not be necessary. * * @param {Image} image */ class CachedImage { /** * @ignore */ constructor() { // eslint-disable-line no-unused-vars this.NUM_ITERATIONS = 4; // Number of items in the coordinates array this.image = new Image(); this.canvas = document.createElement('canvas'); } /** * Called when the image has been successfully loaded. */ init() { if (this.initialized()) return; this.src = this.image.src; // For same interface with Image var w = this.image.width; var h = this.image.height; // Ease external access this.width = w; this.height = h; var h2 = Math.floor(h/2); var h4 = Math.floor(h/4); var h8 = Math.floor(h/8); var h16 = Math.floor(h/16); var w2 = Math.floor(w/2); var w4 = Math.floor(w/4); var w8 = Math.floor(w/8); var w16 = Math.floor(w/16); // Make canvas as small as possible this.canvas.width = 3*w4; this.canvas.height = h2; // Coordinates and sizes of images contained in the canvas // Values per row: [top x, left y, width, height] this.coordinates = [ [ 0 , 0 , w2 , h2], [ w2 , 0 , w4 , h4], [ w2 , h4, w8 , h8], [ 5*w8, h4, w16, h16] ]; this._fillMipMap(); } /** * @return {Boolean} true if init() has been called, false otherwise. */ initialized() { return (this.coordinates !== undefined); } /** * Redraw main image in various sizes to the context. * * The rationale behind this is to reduce artefacts due to interpolation * at differing zoom levels. * * Source: http://stackoverflow.com/q/18761404/1223531 * * This methods takes the resizing out of the drawing loop, in order to * reduce performance overhead. * * TODO: The code assumes that a 2D context can always be gotten. This is * not necessarily true! OTOH, if not true then usage of this class * is senseless. * * @private */ _fillMipMap() { var ctx = this.canvas.getContext('2d'); // First zoom-level comes from the image var to = this.coordinates[0]; ctx.drawImage(this.image, to[0], to[1], to[2], to[3]); // The rest are copy actions internal to the canvas/context for (let iterations = 1; iterations < this.NUM_ITERATIONS; iterations++) { let from = this.coordinates[iterations - 1]; let to = this.coordinates[iterations]; ctx.drawImage(this.canvas, from[0], from[1], from[2], from[3], to[0], to[1], to[2], to[3] ); } } /** * Draw the image, using the mipmap if necessary. * * MipMap is only used if param factor > 2; otherwise, original bitmap * is resized. This is also used to skip mipmap usage, e.g. by setting factor = 1 * * Credits to 'Alex de Mulder' for original implementation. * * @param {CanvasRenderingContext2D} ctx context on which to draw zoomed image * @param {Float} factor scale factor at which to draw * @param {number} left * @param {number} top * @param {number} width * @param {number} height */ drawImageAtPosition(ctx, factor, left, top, width, height) { if(!this.initialized()) return; //can't draw image yet not intialized if (factor > 2) { // Determine which zoomed image to use factor *= 0.5; let iterations = 0; while (factor > 2 && iterations < this.NUM_ITERATIONS) { factor *= 0.5; iterations += 1; } if (iterations >= this.NUM_ITERATIONS) { iterations = this.NUM_ITERATIONS - 1; } //console.log("iterations: " + iterations); let from = this.coordinates[iterations]; ctx.drawImage(this.canvas, from[0], from[1], from[2], from[3], left, top, width, height ); } else { // Draw image directly ctx.drawImage(this.image, left, top, width, height); } } } export default CachedImage;