Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript (jQuery) Scale an Image to Its Container's Center Point

This seems like it should be quite simple, but for some reason I can't quite wrap my brain around it. I have an image inside a "viewport" div, of which the overflow property is set to hidden.

I've implemented a simple zooming and panning with jQuery UI, however I am having trouble getting the zoom to appear to originate from the center of the viewport. I did a little screencast from Photoshop the effect I'm trying to reproduce: http://dl.dropbox.com/u/107346/share/reference-point-zoom.mov

In PS you can adjust the scaling reference point an the object will scale from that point. Obviously this is not possible with HTML/CSS/JS, so I'm trying to find the appropriate left and top CSS values to mimic the effect.

Here is the code in question, with a few unnecessary bits removed:

html

<div id="viewport">
    <img id="map" src="http://dl.dropbox.com/u/107346/share/fake-map.png" alt="" />
</div>

<div id="zoom-control"></div>

javascript

$('#zoom-control').slider({
    min: 300,
    max: 1020,
    value: 300,
    step: 24,
    slide: function(event, ui) {
        var old_width = $('#map').width();
        var new_width = ui.value;
        var width_change = new_width - old_width;
        $('#map').css({
            width: new_width,

            // this is where I'm stuck...
            // dividing by 2 makes the map zoom
            // from the center, but if I've panned
            // the map to a different location I'd
            // like that reference point to change.
            // So instead of zooming relative to
            // the map image center point, it would
            // appear to zoom relative to the center
            // of the viewport. 
            left: "-=" + (width_change / 2),
            top: "-=" + (width_change / 2)
        });
    }
});

Here is the project on JSFiddle: http://jsfiddle.net/christiannaths/W4seR/

like image 706
Christian Avatar asked Dec 30 '11 10:12

Christian


2 Answers

Here's the working solution. I will explain the logic at the next edit.

Function Logic:

  • Summary: Remember the center position of the image, relatively.
    The calculations for width and height are similar, I will only explain the height calculation
    The detailled explanation is just an example of function logic. The real code, with different variable names can be found at the bottom of the answer.

    1. Calculate the center (x,y) of the #map, relative to #viewport. This can be done by using the offset(), height() and width() methods.

      // Absolute difference between the top border of #map and #viewport
      var differenceY = viewport.offset().top - map.offset().top;
      // We want to get the center position, so add it.
      var centerPosition = differenceY + viewport.height() * 0.5;
      // Don't forget about the border (3px per CSS)
      centerPosition += 3;
      // Calculate the relative center position of #map
      var relativeCenterY = centerPosition / map.height();
      // RESULT: A relative offset. When initialized, the center of #map is at
      //  the center of #viewport, so 50% (= 0.5)
      // Same method for relativeCenterX
      
    2. Calculate the new top and left offsets:

      // Calculate the effect of zooming (example: zoom 1->2 = 2)
      var relativeChange = new_width / old_width;
      // Calculate the new height
      var new_height = relativeChange * old_height;
      // Calculate the `top` and `left` CSS properties.
      // These must be negative if the upperleft corner is outside he viewport
      // Add 50% of the #viewport's height to correctly position #map
      //   (otherwise, the center will be at the upperleft corner)
      var newTopCss = -relativeCenterY * new_height + 0.5 * viewport.height();
      
    3. Change the CSS property

      map.css("top", newTopCss);
      

Demo: http://jsfiddle.net/W4seR/12/

var map = $('#map');
var viewport = $('#viewport');
// Cache the size of the viewport (300x300)
var viewport_size = {
    x: viewport.width(),
    y: viewport.height()
};

map.draggable();

$('#zoom-control').slider({
    min: 300,
    max: 1020,
    value: 300,
    step: 24,
    create: function() {
        map.css({
            'width': 300,
            'left': 0,
            'top': 0
        });
    },
    slide: function(event, ui) {
        var old_width = map.width();
        var old_height = map.height();
        var viewport_offset = viewport.offset();
        var offset = map.offset();
        offset = {
            top: viewport_offset.top - offset.top + .5*viewport_size.y +3,
            left: viewport_offset.left - offset.left + .5*viewport_size.x +3
        };
        // Relative offsets, relative to the center!
        offset.top = offset.top / old_height;
        offset.left = offset.left / old_width;

        var new_width = ui.value;
        var relative = new_width / old_width;
        var new_height = relative * old_height;

        offset = {
            top: -offset.top * new_height + .5*viewport_size.y,
            left: -offset.left * new_width + .5*viewport_size.x
        };

        var css_properties = {
            width: new_width,
            left: offset.left,
            top: offset.top
        };

        map.css(css_properties);

        trace((map.position().left));
    }
});
like image 146
Rob W Avatar answered Oct 21 '22 04:10

Rob W


I have always relied on the kindness of strangers. Pertinent changes:

// Calculate the offset as a percentage, accounting for the height of the window
var x_offset = ((map.position().left-150))/(old_width/2);
var y_offset = ((map.position().top-150))/(old_width/2);

var css_properties = {
    width: new_width,
    // Set the offset based on the existing percentage rather than 1/2
    // then readjust for the height of the window
    left: (new_width * x_offset /2 ) + 150 + "px", 
    top: (new_width * y_offset /2 ) + 150 + "px"
};

Replace the hardcoded 150 with a variable set on viewport instantiation if necessary.

like image 38
RSG Avatar answered Oct 21 '22 06:10

RSG