El principio de este plugin es tomar un contenedor del que se quiere hacer un minimapa e indicando un canvas de destino, pinta sobre él los elementos cuyos selectores hayamos definido usando el estilo que deseemos.
Usarlo es lo más sencillo del mundo. Dados un canvas #myCanvas y un contenedor
var minimapInstance = $('#myCanvas').minimap( $('myContainer') [, options] );
Las opciones son opcionales, pero recomendables pues es donde se definen qué elementos se representarán en el minimapa y cómo se hará. Concretamente, la encargada de ello es la propiedad "style" que es un array de objetos con la siguiente estructura:
styleDef { // selector to use for that style selector: String / HTMLElement / [HTMLElement, ...] / jQuery // border stroke width strokeWidth: float / undefined / 'auto' // border color strokeStyle: String / undefined / 'auto' // fill color fillStyle: String / undefined / 'auto' }
Como una imagen vale más que mil palabras, como es usual, el repositorio del código está en mi jsFiddle. Solo hace falta el código javascript.
El código completo es éste
/** * jquery.minimap-0.1.js - Container Minimap Plugin * ========================================================== * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com * * http://3nibbles.blogspot.com/jquery-canvas-minimap.html * http://plugins.jquery.com/project/CanvasMinimap * * Container Minimap is a plugin that creates a minimap of the desired container * scaled down and styled so you can highlight the overall position of the * contents of the container element. * * Scale is calculated based on the minumum of width or height of the canvas used * to draw the minimap. So, using only CSS or attributes you can modify the scale * adjusting it to the desired size of the container. * * INSTANTIATION * Call the minimap method over the selector of the canvas where the minimap is * going to be drawn * * minimapInstance = $('#myCanvas').minimap( $('myContainer') [, options] ); * * The only HTML needed is the target canvas and the container to be drawn. * * OPTIONS * - container : document. Defaults to whole page. Otherwise is a * selector of the desired container to be mapped. * - styles : [ styleDef, ... ] array with the styles to apply. * - viewportStyle : styleDef. Style of the viewport. * - viewportDragStyle: styleDef. Style of the viewport * * The OutlineStyle style must be in the following format (Same format than Fracs): * * styleDef { * // selector to use for that style * selector: String / HTMLElement / [HTMLElement, ...] / jQuery * * // border stroke width * strokeWidth: float / undefined / 'auto' * * // border color * strokeStyle: String / undefined / 'auto' * * // fill color * fillStyle: String / undefined / 'auto' * } * * strokeWidth, strokeColor, fillColor may have the special values * undefined to ignore and 'auto' to fetch values from css. * * PUBLIC API * - $.minimap.redraw() Redraws the minimap * - drag Boolean that indicates that the viewport is being dragged * * Legal stuff * Based on jQuery plugin named Fracs, but modified to allow minimapping of a * desired container and some other enhancements. * * You are free to use this code, but you must give credit and/or keep header intact. * Please, tell me if you find it useful. An email will be enough. * If you enhance this code or correct a bug, please, tell me. */ (function( $ ) { var $window = $(window) , $document = $(document) ; //////////////////////////////////////////////////////////////////////////////// Rect = function (left, top, width, height) { if (!(this instanceof Rect)) { return new Rect(left, top, width, height); } this.left = Math.round( left ); this.top = Math.round( top ); this.width = Math.round( width ); this.height = Math.round( height ); this.right = this.left + this.width; this.bottom = this.top + this.height; }; Rect.prototype = { equals: function( that ) { return this.left === that.left && this.top === that.top && this.width === that.width && this.height === that.height; }, area: function() { return this.width * this.height; }, intersection: function( rect ) { var left = Math.max( this.left , rect.left ), right = Math.min( this.right , rect.right ), top = Math.max( this.top , rect.top ), bottom = Math.min( this.bottom, rect.bottom ), width = right - left, height = bottom - top; return ( width >= 0 && height >= 0 ) ? Rect( left, top, width, height ) : undefined; }, envelope: function( rect ) { var left = Math.min( this.left , rect.left ), right = Math.max( this.right , rect.right ), top = Math.min( this.top , rect.top ), bottom = Math.max( this.bottom, rect.bottom ), width = right - left, height = bottom - top; return Rect( left, top, width, height ); } }; /** * Special constructors */ Rect.ofDocument = function() { return Rect( 0, 0, $document.width(), $document.height() ); }; Rect.ofViewport = function() { return Rect( $window.scrollLeft(), $window.scrollTop(), $window.width(), $window.height() ); }; Rect.ofElement = function( element ) { var $element = $(element) , offset; if( !$element.is(":visible") ) { return Rect( 0, 0, -1, 0 ); } offset = $element.offset(); return Rect( offset.left, offset.top, $element.outerWidth(), $element.outerHeight() ); }; Rect.ofElementOS = function( element, contOffset ) { var $element = $(element) , offset; if( typeof contOffset === 'undefined' || !contOffset ) contOffset = { left: 0, top: 0 }; if( !$element.is(":visible") ) { return Rect( 0, 0, -1, 0 ); } offset = $element.offset(); return Rect( offset.left - contOffset.left, offset.top - contOffset.top, $element.outerWidth(), $element.outerHeight() ); }; //////////////////////////////////////////////////////////////////////////////// // Instantiation $.fn.minimap = function( container, options ) { var contOffset; // Private members function MiniMap( canvas, container, options ) { if ( !(this instanceof MiniMap) ) { return new MiniMap( canvas, container, options ); } if ( !canvas.nodeName || canvas.nodeName.toLowerCase() !== "canvas" ) { return undefined; } var me = this , $canvas = $(canvas) , $container = $(container) || document , width = $canvas.width() || $canvas.attr("width") , height = $canvas.height() || $canvas.attr("height") , context = canvas.getContext("2d") , drag = false , vpRect , scale ; options = $.extend( {}, $.fn.minimap.defaults, options ); // ======================================================================== // Public Members this.redraw = function() { // Gets the container offset to transform contents position contOffset = $(container).offset(); if( !contOffset ) contOffset = { left: 0, top: 0 }; // Gets the Rect of the container docRec = null; if( $container === document ) docRect = Rect.ofDocument(); else docRect = Rect.ofElement( $container ); vpRect = Rect.ofViewport(); scale = Math.min(width / docRect.width, height / docRect.height); // Scales canvas on size and on inner transform $canvas.height( docRect.height * scale ); $canvas.width( docRect.width * scale ); context.setTransform( 1, 0, 0, 1, 0, 0 ); context.clearRect( 0, 0, $canvas.width(), $canvas.height() ); //context.scale( scale, scale ); // WRONG! deforms the projection context.scale( width / docRect.width, height / docRect.height ); // Draws the minimap applyStyles(); //drawViewport(); }, drawRect = function( rect, strokeWidth, strokeStyle, fillStyle, invert ) { // Draws a Rect on canvas if ( strokeStyle || fillStyle ) { if (fillStyle) { context.beginPath(); if (invert) { context.rect( 0, 0, docRect.width, rect.top ); context.rect( 0, rect.top, rect.left, rect.height ); context.rect( rect.right, rect.top, docRect.right - rect.right, rect.height ); context.rect( 0, rect.bottom, docRect.width, docRect.bottom - rect.bottom ); } else { context.rect( rect.left, rect.top, rect.width, rect.height ); } context.fillStyle = fillStyle; context.fill(); } if (strokeStyle) { context.beginPath(); context.rect( rect.left, rect.top, rect.width, rect.height ); context.lineWidth = scale ? Math.max( strokeWidth, 0.2 / scale ) : strokeWidth; context.strokeStyle = strokeStyle; context.stroke(); } } }, drawElement = function( element, strokeWidth, strokeStyle, fillStyle ) { // Gets the Rect of an element and draws it on canvas var $element = $(element), rect = Rect.ofElementOS( element, contOffset ); //rect = Rect.ofElement( element ); if ($element.css("visibility") === "hidden" || rect.width === 0 || rect.height === 0) { return; } strokeWidth = strokeWidth === "auto" ? parseInt($element.css("border-top-width"), 10) : strokeWidth; strokeStyle = strokeStyle === "auto" ? $element.css("border-top-color") : strokeStyle; fillStyle = fillStyle === "auto" ? $element.css("background-color") : fillStyle; drawRect(rect, strokeWidth, strokeStyle, fillStyle); }, applyStyles = function () { // Loops through the contents of the container $.each( options.styles, function (idx, style) { $(style.selector).each(function () { drawElement( this, style.strokeWidth, style.strokeStyle, style.fillStyle ); }); }); }, drawViewport = function () { var style = drag && options.viewportDragStyle ? options.viewportDragStyle : options.viewportStyle; drawRect( vpRect, style.strokeWidth, style.strokeStyle, style.fillStyle, options.invertViewport ); }; me.redraw(); } return new MiniMap( this.get(0), container, options ); }; // ======================================================================== // Minimap options defaults $.fn.minimap.defaults = { container : document, // Defaults to whole page. Otherwise is a selector of the container styles : [{ // Sample styles selector: "header,footer,section,article", fillStyle: "rgb(230,230,230)" }, { selector: "h1", fillStyle: "rgb(240,140,060)" }, { selector: "h2", fillStyle: "rgb(200,100,100)" }, { selector: "h3", fillStyle: "rgb(100,200,100)" }, { selector: "h4", fillStyle: "rgb(100,100,200)" }] }; }(jQuery));
Y el código minimizado que llega a ocupar menos de 1.5Kb es
/** !jquery.minimap-0.1.js - Container Minimap Plugin * (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com * http://3nibbles.blogspot.com/jquery-canvas-minimap.html | http://plugins.jquery.com/project/CanvasMinimap * * Based on jQuery plugin named Fracs, but modified to allow minimapping of a desired container and some other enhancements. * You are free to use this code, but you must give credit and/or keep header intact. */ (function(f){var h=f(window),j=f(document);Rect=function(a,b,c,e){if(!(this instanceof Rect))return new Rect(a,b,c,e);this.left=Math.round(a);this.top=Math.round(b);this.width=Math.round(c);this.height=Math.round(e);this.right=this.left+this.width;this.bottom=this.top+this.height};Rect.prototype={equals:function(a){return this.left===a.left&&this.top===a.top&&this.width===a.width&&this.height===a.height},area:function(){return this.width*this.height},intersection:function(a){var b=Math.max(this.left, a.left),c=Math.min(this.right,a.right),e=Math.max(this.top,a.top),a=Math.min(this.bottom,a.bottom);c-=b;a-=e;return c>=0&&a>=0?Rect(b,e,c,a):void 0},envelope:function(a){var b=Math.min(this.left,a.left),c=Math.max(this.right,a.right),e=Math.min(this.top,a.top),a=Math.max(this.bottom,a.bottom);return Rect(b,e,c-b,a-e)}};Rect.ofDocument=function(){return Rect(0,0,j.width(),j.height())};Rect.ofViewport=function(){return Rect(h.scrollLeft(),h.scrollTop(),h.width(),h.height())};Rect.ofElement=function(a){var a= f(a),b;if(!a.is(":visible"))return Rect(0,0,-1,0);b=a.offset();return Rect(b.left,b.top,a.outerWidth(),a.outerHeight())};Rect.ofElementOS=function(a,b){var c=f(a),e;if(typeof b==="undefined"||!b)b={left:0,top:0};if(!c.is(":visible"))return Rect(0,0,-1,0);e=c.offset();return Rect(e.left-b.left,e.top-b.top,c.outerWidth(),c.outerHeight())};f.fn.minimap=function(a,b){function c(a,b,i){if(!(this instanceof c))return new c(a,b,i);if(a.nodeName&&a.nodeName.toLowerCase()==="canvas"){var g=f(a),h=f(b)||document, j=g.width()||g.attr("width"),l=g.height()||g.attr("height"),d=a.getContext("2d"),m,k,i=f.extend({},f.fn.minimap.defaults,i);this.redraw=function(){(e=f(b).offset())||(e={left:0,top:0});docRec=null;docRect=h===document?Rect.ofDocument():Rect.ofElement(h);m=Rect.ofViewport();k=Math.min(j/docRect.width,l/docRect.height);g.height(docRect.height*k);g.width(docRect.width*k);d.setTransform(1,0,0,1,0,0);d.clearRect(0,0,g.width(),g.height());d.scale(j/docRect.width,l/docRect.height);applyStyles()};drawRect= function(a,b,c,e,f){if(c||e){if(e)d.beginPath(),f?(d.rect(0,0,docRect.width,a.top),d.rect(0,a.top,a.left,a.height),d.rect(a.right,a.top,docRect.right-a.right,a.height),d.rect(0,a.bottom,docRect.width,docRect.bottom-a.bottom)):d.rect(a.left,a.top,a.width,a.height),d.fillStyle=e,d.fill();if(c)d.beginPath(),d.rect(a.left,a.top,a.width,a.height),d.lineWidth=k?Math.max(b,0.2/k):b,d.strokeStyle=c,d.stroke()}};drawElement=function(a,b,c,d){var g=f(a),a=Rect.ofElementOS(a,e);g.css("visibility")==="hidden"|| a.width===0||a.height===0||(b=b==="auto"?parseInt(g.css("border-top-width"),10):b,c=c==="auto"?g.css("border-top-color"):c,d=d==="auto"?g.css("background-color"):d,drawRect(a,b,c,d))};applyStyles=function(){f.each(i.styles,function(a,b){f(b.selector).each(function(){drawElement(this,b.strokeWidth,b.strokeStyle,b.fillStyle)})})};drawViewport=function(){var a=i.viewportStyle;drawRect(m,a.strokeWidth,a.strokeStyle,a.fillStyle,i.invertViewport)};this.redraw()}}var e;return new c(this.get(0),a,b)};f.fn.minimap.defaults= {container:document,styles:[{selector:"header,footer,section,article",fillStyle:"rgb(230,230,230)"},{selector:"h1",fillStyle:"rgb(240,140,060)"},{selector:"h2",fillStyle:"rgb(200,100,100)"},{selector:"h3",fillStyle:"rgb(100,200,100)"},{selector:"h4",fillStyle:"rgb(100,100,200)"}]}})(jQuery);
Éste ha quedado chulo, ein?