/** * 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. */ class CachedImage { constructor(image) { 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 succesfully loaded. */ init() { if (this.initialized()) return; var w = this.image.width; var h = this.image.height; // Ease external access this.width = w; this.height = h; // Make canvas as small as possible this.canvas.width = 3*w/4; this.canvas.height = h/2; // Coordinates and sizes of images contained in the canvas // Values per row: [top x, left y, width, height] this.coordinates = [ [ 0 , 0 , w/2 , h/2], [ w/2 , 0 , w/4 , h/4], [ w/2 , h/4, w/8 , h/8], [ 5*w/8, h/4, w/16, h/16] ]; 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. * * @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. * * ctx {Context} context on which to draw zoomed image * factor {Float} scale factor at which to draw */ drawImageAtPosition(ctx, factor, left, top, width, height) { if (factor > 2 && this.initialized()) { // 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 if (this._isImageOk()) { // Draw image directly ctx.drawImage(this.image, left, top, width, height); } } /** * Check if image is loaded * * Source: http://stackoverflow.com/a/1977898/1223531 * * @private */ _isImageOk(img) { var img = this.image; // During the onload event, IE correctly identifies any images that // weren’t downloaded as not complete. Others should too. Gecko-based // browsers act like NS4 in that they report this incorrectly. if (!img.complete) { return false; } // However, they do have two very useful properties: naturalWidth and // naturalHeight. These give the true size of the image. If it failed // to load, either of these should be zero. if (typeof img.naturalWidth !== "undefined" && img.naturalWidth === 0) { return false; } // No other way of checking: assume it’s ok. return true; } } export default CachedImage;