Don't like ads? PRO users don't see any ads ;-)
Guest

Untitled

By: a guest on Sep 7th, 2012  |  syntax: None  |  size: 8.92 KB  |  hits: 6  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. var url = 'http://farm8.staticflickr.com/7097/7357430266_54bf1e7069_b.jpg'
  2.   , zoom
  3.   , Zoomer = Backbone.View.extend(
  4.     { events:
  5.       { 'click button.zoom-in':   'zoomIn'
  6.       , 'click button.zoom-out':  'zoomOut'
  7.       , 'mousewheel .spill-zone': 'zoom'
  8.       , 'mousedown .spill-zone':  'dragStart'
  9.       }
  10.  
  11.     // used solely for decoding what image_scale===1 means, for some given image
  12.     , page_w: 960
  13.     , page_h: 640
  14.  
  15.     , $bg: null // this widget's zoomable image
  16.  
  17.     , initialize: function(img_opts) {
  18.         _.bindAll( this, 'load', 'place' // fetching the image
  19.                  , 'input', 'output' // data conversion, to/from site format
  20.                  , 'zoom', 'zoomIn', 'zoomOut', 'zoomBy', 'legalCoords'
  21.                  , 'dragStart', 'dragMove', 'dragEnd'
  22.                  );
  23.  
  24.         this.$bg    = this.$('.zoomer img');
  25.         this.$z_in  = this.$('button.zoom-in');
  26.         this.$z_out = this.$('button.zoom-out');
  27.  
  28.         // undefined, if first time we load a picture, otherwise set in 'input':
  29.         this.height = // original image height
  30.         this.width  = // original image width
  31.         this.dx     = // hor. offset of image tag
  32.         this.dy     = // vert offset of image tag
  33.         this.zoom_w = undefined; // width, zoomed
  34.         if ('image_scale' in img_opts) this.input(img_opts);
  35.  
  36.         // this.load (really this.place) sets these, once we have all details
  37.         this.min_w  =
  38.         this.max_w  = undefined;
  39.         this.load(img_opts.url);
  40.       }
  41.  
  42.     , input: function(data) {
  43.         // this code corresponds loosely to app/helpers/pages_helper.rb's ie8
  44.         // fallback, where we're back-computing what a scale-from-center of s
  45.         // does to the top / left coordinates
  46.         var img_w   = this.width  = data.width
  47.           , img_h   = this.height = data.height
  48.           , delta   = (data.image_scale - 1) / 2 // coord. translation factor
  49.  
  50.           // a page is page_w * page_h, which becomes boxed.width * boxed.height
  51.           // at the minimum zoom level needed to attain our full-frame fill-crop
  52.           , boxed   = constrain(img_w, img_h, this.page_w, this.page_h)
  53.           ;
  54.         this.dx     = Math.round(data.offset_x - delta * boxed.width);
  55.         this.dy     = Math.round(data.offset_y - delta * boxed.height);
  56.         this.zoom_w = Math.round(data.image_scale * boxed.width);
  57.       }
  58.     , output: function() {
  59.         var boxed = constrain(this.width, this.height, this.page_w, this.page_h)
  60.           , base  = boxed.width / this.width // baseline image width at pg scale
  61.           , scale = this.zoom_w / this.width / base
  62.           , delta = (this.zoom_w / boxed.width - 1) / 2
  63.           ;
  64.         return { offset_x: Math.round(this.dx + boxed.width  * (scale-1) / 2)
  65.                , offset_y: Math.round(this.dy + boxed.height * (scale-1) / 2)
  66.                , image_scale: scale
  67.                };
  68.       }
  69.  
  70.     , load: function(url) {
  71.         var img = this.$bg.get(0);
  72.         img.src = url;
  73.         if (img.naturalWidth)
  74.           this.place();
  75.         else
  76.           this.$bg.load(this.place);
  77.       }
  78.     , place: function(dx, dy) {
  79.         if (!_.isNumber(dx) || !_.isNumber(dy)) dx = dy = 0;
  80.         var self   = this
  81.           , $bg    = this.$bg
  82.           , $box   = $bg.parent()
  83.           , box_w  = $box.width(),  mid_x = box_w >> 1
  84.           , box_h  = $box.height(), mid_y = box_h >> 1
  85.           , img    = $bg.get(0)
  86.           , img_h  = Math.round(this.zoom_w * this.height / this.width)
  87.           , boxed, w, h
  88.           ;
  89.  
  90.         if (!_.isNumber(this.zoom_w)) { // fill-crop to mid part of image
  91.           this.width  = w = img.naturalWidth;
  92.           this.height = h = img.naturalHeight;
  93.           boxed       = constrain(this.width, this.height, box_w, box_h);
  94.           this.zoom_w = boxed.width;
  95.           this.dx     = parseInt(boxed['margin-left'] || '0', 10);
  96.           this.dy     = parseInt(boxed['margin-top']  || '0', 10);
  97.         }
  98.         $bg.css(this.legalCoords({ width: this.zoom_w
  99.                                  , left:  this.dx + dx
  100.                                  , top:   this.dy + dy
  101.                                  }));
  102.  
  103.         $.each(this.output(), function debug(k, val) {
  104.           self.$('.debug .' + k).text(val.toFixed(k === 'image_scale' ? 2 : 0));
  105.         });
  106.  
  107.         // bounds of the zoom widget; zooming outside of this range not possible
  108.         if (!this.min_w) {
  109.           boxed = boxed || constrain(this.width, this.height, box_w, box_h);
  110.           this.min_w = boxed.width;
  111.           this.max_w = Infinity; // FIXME: would be nicer MAX_ZOOM_FACTOR based
  112.           // console.log( 'min:', this.min_w
  113.           //            , 'max:', this.max_w);
  114.         }
  115.  
  116.         // show visually when you can't zoom any deeper / further out
  117.         this.$z_in.prop( 'disabled', this.zoom_w >= this.max_w);
  118.         this.$z_out.prop('disabled', this.zoom_w <= this.min_w);
  119.       }
  120.  
  121.     , legalCoords: function(data) {
  122.         var $box  = this.$bg.parent()
  123.           , img_h = Math.round(this.zoom_w * this.height / this.width)
  124.           ;
  125.         data.left = confine(data.left, [$box.width() - this.zoom_w, 0]);
  126.         data.top  = confine(data.top,  [$box.height() - img_h,      0]);
  127.         return data;
  128.       }
  129.  
  130.     , dragStart: function(e) {
  131.         var drag = '.zoom-drag-'+ this.cid; // scope our event listeners
  132.         $(window).bind('mouseup' + drag,    this.dragEnd)
  133.                  .bind('mouseleave' + drag, this.dragEnd)
  134.                  .bind('mousemove' + drag,  this.dragMove);
  135.         this.x = e.clientX;
  136.         this.y = e.clientY;
  137.         e.preventDefault();
  138.       }
  139.     , dragEnd: function(e) {
  140.         $(window).unbind('.zoom-drag-'+ this.cid);
  141.         e.preventDefault();
  142.         var pos = this.legalCoords({ left: this.dx + e.clientX - this.x
  143.                                    , top:  this.dy + e.clientY - this.y });
  144.         this.dx = pos.left;
  145.         this.dy = pos.top;
  146.         this.place();
  147.       }
  148.     , dragMove: function(e) {
  149.         this.place(e.clientX - this.x, e.clientY - this.y);
  150.       }
  151.  
  152.     , zoom: function(e) {
  153.         var orig = e.originalEvent
  154.           , dy   = orig.wheelDeltaY ? orig.wheelDeltaY / 120 :
  155.                    orig.wheelDelta  ? orig.wheelDelta  / 120 :
  156.                    orig.detail      ? orig.detail      / 3   : 0
  157.           , zoom = dy<0 ? 'zoomOut' : 'zoomIn'
  158.           ;
  159.         if (zoom) {
  160.           e.preventDefault();
  161.           this[zoom](e);
  162.         }
  163.       }
  164.     , zoomIn: function(e) {
  165.         if (this.zoomBy(e, +1)) this.place();
  166.       }
  167.     , zoomOut: function(e) {
  168.         if (this.zoomBy(e, -1)) this.place();
  169.       }
  170.     // picks amount of zoom and tries to zoom, relative to the viewport's center
  171.     // (returns the amount of change to the image width -- so 0 means no change)
  172.     , zoomBy: function(e, sign) {
  173.         // for more precision - hold shift, for fast zooming - hold alt
  174.         var shift = e.shiftKey ? 1   : 10
  175.           , alt   = e.metaKey  ? 100 : 1
  176.           , amt   = sign * alt * shift
  177.  
  178.           // do the actual zoom (within bounds)
  179.           , old_w = this.zoom_w
  180.           , old_h = this.zoom_w * this.height / this.width
  181.           , new_w = this.zoom_w = confine(old_w + amt, [this.min_w, this.max_w])
  182.           , new_h = this.zoom_w * this.height / this.width
  183.           , delta = new_w - old_w
  184.  
  185.           // what coordinate should the current centerpoint be in the new image?
  186.           , $box  = this.$bg.parent()
  187.           , box_w = $box.width()
  188.           , box_h = $box.height()
  189.           , x_dom = [-new_w + box_w / 2, box_w / 2]
  190.           , y_dom = [-new_h + box_h / 2, box_h / 2]
  191.           // FIXME: the centering math here needs rethinking somehow
  192.           , pct_x = pct(confine(this.dx - box_w / 2, x_dom), x_dom)
  193.           , pct_y = pct(confine(this.dy - box_h / 2, y_dom), y_dom)
  194.           ;
  195.  
  196.         function pct(n, domain) {
  197.           var total = domain[1] - domain[0];
  198.           return (n - domain[0]) / total;
  199.         }
  200.  
  201.         //console.log(this.dx, this.dy, pct_x.toFixed(4), pct_y.toFixed(4));
  202.  
  203.         this.dx += delta * pct_x;
  204.         this.dy += delta * pct_y;
  205.         return delta;
  206.       }
  207.   })
  208.   ;
  209.  
  210. $(document).ready(function() {
  211.   zoom = new Zoomer({ el:          $('#page-1').get(0)
  212.                     , url:         url
  213.                   //, image_scale: 1.8
  214.                   //, offset_x:    -255
  215.                   //, offset_y:    -354
  216.                   //, width:       785
  217.                   //, height:      594
  218.                     });
  219. });
  220.  
  221. function constrain(img_w, img_h, to_w, to_h) {
  222.   var box_w = (to_w || 800)
  223.     , box_h = (to_h || 600)
  224.  
  225.     // aspect ratios
  226.     , box_a = box_w / box_h
  227.     , img_a = img_w / img_h
  228.  
  229.     // only one of these is used, depending on the image and box aspect ratios:
  230.     , new_w = Math.floor(box_h * img_a)
  231.     , new_h = Math.floor(box_w / img_a)
  232.     ;
  233.   return img_a < box_a ?
  234.     { width: box_w, height: new_h, 'margin-top':  (box_h - new_h) >> 1 }
  235.   : { width: new_w, height: box_h, 'margin-left': (box_w - new_w) >> 1 };
  236. }
  237.  
  238. function confine(n, domain) {
  239.   return Math.max(domain[0], Math.min(n, domain[1]));
  240. }