// ========================================================================== // project: image scale // description: scale images to fit or fill any target size via two simple properties: scale and align. // copyright: ©2012-2016 gestixi // license: licensed under the mit license (see licence) // version: 2.2 // author: nicolas badia // ========================================================================== // uses commonjs, amd or browser globals to create a jquery plugin. (function(factory) { if (typeof define === 'function' && define.amd) { // amd. register as an anonymous module. define(['jquery'], factory); } else if (typeof module === 'object' && module.exports) { // node/commonjs module.exports = function( root, jquery ) { if ( jquery === undefined ) { // require('jquery') returns a factory that requires window to // build a jquery instance, we normalize how we use modules // that require this pattern but the window provided is a noop // if it's defined (how jquery works) if (typeof window !== 'undefined') { jquery = require('jquery'); } else { jquery = require('jquery')(root); } } factory(jquery); return jquery; }; } else { // browser globals factory(jquery); } }(function($) { "use strict"; // .......................................................... // image scale plugin definition // $.fn.imagescale = function( options ) { return this.each(function() { var that = this, $this = $(this), data = $this.data('imagescale'), $img = this.tagname === 'img' ? $this : $this.find("img"); if (!data) { var didload = $img[0].complete, formattedopt = $.extend({}, $.fn.imagescale.defaults, typeof options == 'object' && options), loadfunc = function() { $this.data('imagescale', (data = new imagescale(that, formattedopt))); data.scale(true, formattedopt); }; if (didload) { loadfunc.apply($this[0]); } else { $img.on("load", loadfunc).attr("src", $img.attr("src")); } } else { if (typeof options == 'string') data[options](); else if (typeof options == 'object') { var method = options.method || 'scale'; data[method](false, options); } else data.scale(); } }) } $.fn.imagescale.defaults = { /** determines how the image will scale to fit within its containing space. possible values: * **fill** - stretches or compresses the source image to fill the target frame * **best-fill** - fits the shortest side of the source image within the target frame while maintaining the original aspect ratio * **best-fit** - fits the longest edge of the source image within the target frame while maintaining the original aspect ratio * **best-fit-down** - same as *best-fit* but will not stretch the source if it is smaller than the target * **none** - the source image is left unscaled @type string @default best-fill @since version 1.2 */ scale: 'best-fill', /** align the image within its frame. possible values: * **left** * **right** * **center** * **top** * **bottom** * **top-left** * **top-right** * **bottom-left** * **bottom-right** @type string @default center @since version 1.2 */ align: 'center', /** a jquery object against which the image size will be calculated. if null, the parent of the image will be used. @type jquery object @default null @since version 1.0 */ parent: null, /** a boolean determining if the parent should hide its overflow. @type boolean @default true @since version 1.0 */ hideparentoverflow: true, /** a duration in milliseconds determining how long the fadein animation will run when your image is scale for the firsttime. set it to 0 if you don't want any animation. @type number|string @default 0 @since version 1.1 */ fadeinduration: 0, /** a boolean indicating if the image size should be rescaled when the window is resized. the window size is checked using requestanimationframe for good performance. example: $images.imagescale({ rescaleonresize: true }); @type boolean @default false @since version 1.0 */ rescaleonresize: true, /** a function that will be call each time the receiver is scaled. example: $images.imagescale({ didscale: function() { console.log('did scale img: ', this.element); } }); @type function @param firsttime {boolean} true if the image was scale for the first time. @param options {object} the options passed to the scale method. @since version 2.0 */ didscale: function(firsttime, options) {}, /** a number indicating the log level : 0: silent 1: error 2: error & warning 3: error & warning & notice @type number @default 0 @since version 1.0 */ loglevel: 0 } // .......................................................... // image scale public class definition // var imagescale = function(element, options) { var that = this; that.options = options; that.element = element; var $element = that.$element = $(element), $img = that.$img = element.tagname === 'img' ? $element : $element.find("img"), img = that.img = $img[0]; that.src = $img.attr('src'); that.imgwidth = img.naturalwidth || img.width; that.imgheight = img.naturalheight || img.height; var $parent = that.$parent = options.parent?options.parent:$($element.parent()[0]); that.parent = $parent[0]; // fixes: https://github.com/gestixi/image-scale/issues/1 if ($parent.css('position') === 'static') { $parent.css('position', 'relative'); } if (options.rescaleonresize) { $(window).resize(function(e) { that.schedulescale(); }); } } $.fn.imagescale.constructor = imagescale; imagescale.prototype = { none: "none", fill: "fill", best_fill: "best-fill", best_fit: "best-fit", best_fit_down_only: "best-fit-down", align_left: 'left', align_right: 'right', align_center: 'center', align_top: 'top', align_bottom: 'bottom', align_top_left: 'top-left', align_top_right: 'top-right', align_bottom_left: 'bottom-left', align_bottom_right: 'bottom-right', constructor: imagescale, /** the initial element. @type dom element */ element: null, /** the passed options. @type object */ options: null, /** main method. used to scale the images. when `rescaleonresize` is set to true, this method is executed each time the windows size changes. if `rescaleonresize` is set to false, you may want to call it manually. here is an example on how you should do it: $image.imagescale('scale'); @param {boolean} firsttime */ scale: function(firsttime, opt) { if (this._isdestroyed || this._canscale === false) return; var that = this, options = this.options, $parent = this.$parent, element = this.element, $element = this.$element, img = this.img, $img = this.$img; if (firsttime) { if (options.hideparentoverflow) { $parent.css({ overflow: 'hidden' }); } } else { // if the source of the image has changed if (this.src !== $img.attr('src')) { this.destroy(); $element.data('imagescale', null); $element.imagescale(options); return; } } this._didschedulescale = false; if (options.rescaleonresize && !opt) { if (!this._needupdate(this.parent)) return; } opt = opt ? opt : {}; var transition = opt.transition; if (transition) { this._canscale = false; $element.css('transition', 'all '+transition+'ms'); settimeout(function() { that._canscale = null; $element.css('transition', 'null'); }, transition); } var destwidth = opt.destwidth ? opt.destwidth : $parent.outerwidth(), destheight = opt.destheight ? opt.destheight : $parent.outerheight(), destinnerwidth = opt.destwidth ? opt.destwidth : $parent.innerwidth(), destinnerheight = opt.destheight ? opt.destheight : $parent.innerheight(), widthoffset = destwidth - destinnerwidth, heightoffset = destheight - destinnerheight, scaledata = $element.attr('data-scale'), aligndata = $element.attr('data-align'), scale = scaledata?scaledata:options.scale, align = aligndata?aligndata:options.align, fadeinduration = options.fadeinduration; if (!scale) { if (options.loglevel > 2) { console.log("imagescale - debug notice: the scale property is null.", element); } return; } if (this._cachedestwidth === destwidth && this._cachedestheight === destheight) { if (options.loglevel > 2) { console.log("imagescale - debug notice: the parent size hasn't changed: dest width: '"+destwidth+"' - dest height: '"+destheight+"'.", element); } } var sourcewidth = this.imgwidth, sourceheight = this.imgheight; if (!(destwidth && destheight && sourcewidth && sourceheight)) { if (options.loglevel > 0) { console.error("imagescale - debug error: the dimensions are incorrect: source width: '"+sourcewidth+"' - source height: '"+sourceheight+"' - dest width: '"+destwidth+"' - dest height: '"+destheight+"'.", element); } return; } this._cachedestwidth = destwidth; this._cachedestheight = destheight; var layout = this._innerframeforsize(scale, align, sourcewidth, sourceheight, destwidth, destheight); if (widthoffset) layout.x -= widthoffset/2; if (heightoffset) layout.y -= heightoffset/2; $element.css({ position: 'absolute', top: layout.y+'px', left: layout.x+'px', width: layout.width+'px', height: layout.height+'px', 'max-width': 'none' }); if (firsttime && fadeinduration) { $element.css({ display: 'none' }); $element.fadein(fadeinduration); } options.didscale.call(this, firsttime, opt); }, /** removes the data from the element. here is an example on how you can call the destroy method: $image.imagescale('destroy'); */ destroy: function() { this._isdestroyed = true; this.$element.removedata('imagescale'); }, /** @private returns a frame (x, y, width, height) fitting the source size (sourcewidth & sourceheight) within the destination size (destwidth & destheight) according to the align and scale properties. @param {string} scale @param {string} align @param {number} sourcewidth @param {number} sourceheight @param {number} destwidth @param {number} destheight @returns {object} the inner frame with properties: { x: value, y: value, width: value, height: value } */ _innerframeforsize: function(scale, align, sourcewidth, sourceheight, destwidth, destheight) { var scalex, scaley, result; // fast path result = { x: 0, y: 0, width: destwidth, height: destheight }; if (scale === this.fill) return result; // determine the appropriate scale scalex = destwidth / sourcewidth; scaley = destheight / sourceheight; switch (scale) { case this.best_fit_down_only: if (scale !== this.best_fit_down_only && this.options.loglevel > 1) { console.warn("imagescale - debug warning: the scale '"+scale+"' was not understood."); } if ((sourcewidth > destwidth) || (sourceheight > destheight)) { scale = scalex < scaley ? scalex : scaley; } else { scale = 1.0; } break; case this.best_fit: scale = scalex < scaley ? scalex : scaley; break; case this.none: scale = 1.0; break; //case this.best_fill: default: scale = scalex > scaley ? scalex : scaley; break; } sourcewidth *= scale; sourceheight *= scale; result.width = math.round(sourcewidth); result.height = math.round(sourceheight); // align the image within its frame switch (align) { case this.align_left: result.x = 0; result.y = (destheight / 2) - (sourceheight / 2); break; case this.align_right: result.x = destwidth - sourcewidth; result.y = (destheight / 2) - (sourceheight / 2); break; case this.align_top: result.x = (destwidth / 2) - (sourcewidth / 2); result.y = 0; break; case this.align_bottom: result.x = (destwidth / 2) - (sourcewidth / 2); result.y = destheight - sourceheight; break; case this.align_top_left: result.x = 0; result.y = 0; break; case this.align_top_right: result.x = destwidth - sourcewidth; result.y = 0; break; case this.align_bottom_left: result.x = 0; result.y = destheight - sourceheight; break; case this.align_bottom_right: result.x = destwidth - sourcewidth; result.y = destheight - sourceheight; break; default: // this.align_center if (align !== this.align_center && this.options.loglevel > 1) { console.warn("imagescale - debug warning: the align '"+align+"' was not understood."); } result.x = (destwidth / 2) - (sourcewidth / 2); result.y = (destheight / 2) - (sourceheight / 2); } return result; }, /** @private determines if the windows size has changed since the last update. @returns {boolean} */ _needupdate: function(parent) { var size = parent.clientheight + ' ' + parent.clientwidth; if (this._lastparentsize !== size) { this._lastparentsize = size; return true; } return false; }, /** @private schedule a scale update. */ schedulescale: function() { if (this._didschedulescale) return; if (window.requestanimationframe) { var that = this; this._didschedulescale = true; // settimeout important when resizing down if the scrollbar were visible requestanimationframe(function() { settimeout(function() { that.scale(); }, 0); }); } else { this.scale(); } } } }));