2011/10/27

jQuery Canvas Minimap

Y para rematar la semana, la joya de la corona. Otro plugin hecho por mí que toma un contenedor y genera un minimapa de éste y que pese a basarse en otro ya existente llamado Fracs, le da un giro de tuerca y lo hace más flexible.

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?

No hay comentarios:

Publicar un comentario