Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep SVG content visible when zooming #37

Closed
ariutta opened this issue Apr 21, 2014 · 21 comments
Closed

Keep SVG content visible when zooming #37

ariutta opened this issue Apr 21, 2014 · 21 comments
Assignees
Milestone

Comments

@ariutta
Copy link
Collaborator

ariutta commented Apr 21, 2014

Right now, it's possible to zoom into a blank area outside the bounding box of the SVG content. The view should never entirely leave the bounding box of the SVG content.

@JamesLefrere
Copy link

Here's one way of getting this done. self.gameWidth and self.gameHeight are the size of the SVG, and the gutter leaves a bit of space to the sides.

var panZoom = svgPanZoom('#game', {
  zoomEnabled: false
});
panZoom.setOnPan(function () {
  var gutter = 100;
  var maxRight = self.gameWidth - gutter;
  var maxLeft = -self.gameWidth + gutter;
  var maxTop = self.gameHeight - gutter;
  var maxBottom = -self.gameHeight + gutter;
  if (this.getPan().x > maxRight)
    this.pan({x: maxRight, y: this.getPan().y});
  if (this.getPan().x < maxLeft)
    this.pan({x: maxLeft, y: this.getPan().y});
  if (this.getPan().y > maxTop)
    this.pan({x: this.getPan().x, y: maxTop});
  if (this.getPan().y < maxBottom)
    this.pan({x: this.getPan().x, y: maxBottom});
});

@ariutta
Copy link
Collaborator Author

ariutta commented May 28, 2014

Cool! I'll add this when I get a minute. Thanks.

On Sun, May 25, 2014 at 7:13 AM, James Lefrère notifications@github.comwrote:

Here's one way of getting this done. self.gameWidth and self.gameHeight
are the size of the SVG.

var panZoom = svgPanZoom('#game', {
zoomEnabled: false
});
panZoom.setOnPan(function () {
var gutter = 100;
var maxRight = self.gameWidth - gutter;
var maxLeft = -self.gameWidth + gutter;
var maxTop = self.gameHeight - gutter;
var maxBottom = -self.gameHeight + gutter;
if (this.getPan().x > maxRight)
this.pan({x: maxRight, y: this.getPan().y});
if (this.getPan().x < maxLeft)
this.pan({x: maxLeft, y: this.getPan().y});
if (this.getPan().y > maxTop)
this.pan({x: this.getPan().x, y: maxTop});
if (this.getPan().y < maxBottom)
this.pan({x: this.getPan().x, y: maxBottom});
});


Reply to this email directly or view it on GitHubhttps://github.com//issues/37#issuecomment-44134132
.

@ledancs
Copy link

ledancs commented Nov 6, 2014

Hey guys! Thanks a lot for an impressive library!
About this issue.... I am having a problem with restricting the view calling this.pan whenever a limit is exceeded. The CPU and memory of my laptop go crazy and the performance is a bit poor when panning.
Any ideas or guidelines in the library side? Perhaps you could point us in the right direction on how to intercept the calls to the pan function whenever we detect an limit has been reached.
Thanks a lot!

@ariutta
Copy link
Collaborator Author

ariutta commented Nov 6, 2014

Thanks - glad you find it useful!

I haven't actually added the code above from James to the library yet. Are you using James's solution to keep the content visible?

@ariutta
Copy link
Collaborator Author

ariutta commented Nov 6, 2014

@JamesLefrere and @bumbu, finally getting to this and wanted to make sure we're on the same page with what to do. My thoughts:

  • This feature is not part of the API. It's enabled by default and runs behind the scenes.
  • Gutter value is 100px
  • Instead of gameWidth/gameHeight, we use the dimensions from SvgUtils.getBoundingClientRectNormalized()

When limited, it'll look like this:
svg-pan-zoom dimensions1

@ledancs
Copy link

ledancs commented Nov 6, 2014

@ariutta I tried to use James's solution but my SVG files use negative coordinates. Also, I am using the whole body element of the DOM as a container ( similar to how google maps look like) . What I tried to do is obtaining the window width and height values before initializing the svg-pan-zoom object and updating those values when the window is resized. I do so in order to calculate the gutter for a resizable window so that at least 1/4 of the SVG is always visible.

@bumbu
Copy link
Owner

bumbu commented Nov 7, 2014

@ariutta: I had the same in mind. But there are 2 things that I would expose to API:

  • Ability to disable this feature (maybe at initialization)
  • Ability to set a custom value for gutter (at initialization time)

But now if @ledancs says that he uses a 1/4 of page as gutter maybe it will be a good idea to allow gutter value to be number or a function?

About SvgUtils.getBoundingClientRectNormalized is cached as this.width and this.height. If you call APIs resize then these values will be racached. So we can use these cached values instead of calling getBoundingClientRectNormalized which is heavy in Firefox.

@ariutta
Copy link
Collaborator Author

ariutta commented Nov 10, 2014

@bumbu, agreed -- ability to disable and to set custom gutter values would be good to include in the API. For specifying gutter, I think a function might be more complex than is needed. What about if the acceptable inputs are CSS lengths like 100px and percentages like 25%. The percentage would be based on the dimensions of the viewport 'g' element from getViewBox().

@ledancs, are you using resize from the API? Also, would the API above work for you?

@bumbu
Copy link
Owner

bumbu commented Nov 11, 2014

@ariutta, I don't see why a function would be too complex if used in pair with numbers.. I just have a feeling that as some point somebody will ask for ability to customize this parameter. By allowing a number of a function we can cover most use-cases:

  • Most users will not know about existence of this parameter and will use default
  • Some users may want to change default gutter in 'px'
  • Few will want full control over gutter (maybe percentage, or maybe different values for top and bottom, or maybe a gutter depending on viewport width and height). For this cases we may allow setting this parameter as a function that will receive as a parameter an object with viewport zoom, width, height (and maybe some other attributes). This function will be called each time viewport pan will be performed.

@abecks
Copy link

abecks commented Nov 12, 2014

I needed to prevent the user from panning away from the SVG viewport. What I did was check to see if the user has panned farther than the current dimensions of the SVG viewport.

This is probably not the most optimal setup and the pan event handler should probably be throttled, but you'll get the idea:

  var $display = this.$("#display");
  var displayElem = $display.get(0);
  var winWidth = $(window).width();
  var winHeight = $(window).height();

  panZoomGalaxy = svgPanZoom(displayElem, {
    fit: false,
    center: false,
    dblClickZoomEnabled: false,
    zoomScaleSensitivity: 0.025,
    minZoom: 1,
    maxZoom: 2,
    onPan: function(x, y){

      var bBox = displayElem.getBBox();

      var panLock = {
        xMin: - (bBox.width - winWidth),
        xMax: 0,
        yMin: - (bBox.height - winHeight),
        yMax: 0
      };

      var repan = false;

      if (x > panLock.xMax) {
        x = panLock.xMax;
        repan = true;
      }
      if (x < panLock.xMin) {
        x = panLock.xMin;
        repan = true;
      }
      if (y < panLock.yMin) {
        y = panLock.yMin;
        repan = true;
      }
      if (y > panLock.yMax) {
        y = panLock.yMax;
        repan = true;
      }

      if (repan) {
        panZoomGalaxy.pan({x: x, y: y});
      }
    }
  });

I'm checking the current size of the SVG element (using getBBox to account for zoom) and then checking to see if the user has panned farther than the width/height of the SVG canvas. If they have, I call pan() manually to reset the position to the closest limit.

It's pretty smooth on Chrome but I'm not sure about other browsers. I'd personally rather cancel the pan event than call pan() manually, but I don't know if thats possible.

I still have to write a handler for zooming to re-pan the SVG if the user zooms out while near an edge. If you zoom in and pan to an edge, then zoom out, you can see outside of the SVG canvas.

@abecks
Copy link

abecks commented Nov 12, 2014

Actually, I just figured out a better way to do it that also keeps the canvas in view while zooming:

var $display = this.$("#display");
  var displayElem = $display.get(0);

  var checkPosition = function(x, y){
    var bBox = displayElem.getBBox();
    var winWidth = $(window).width();
    var winHeight = $(window).height();

    var panLock = {
      xMin: - (bBox.width - winWidth),
      xMax: 0,
      yMin: - (bBox.height - winHeight),
      yMax: 0
    };

    var repan = false;

    if (x > panLock.xMax) {
      x = panLock.xMax;
      repan = true;
    }
    if (x < panLock.xMin) {
      x = panLock.xMin;
      repan = true;
    }
    if (y < panLock.yMin) {
      y = panLock.yMin;
      repan = true;
    }
    if (y > panLock.yMax) {
      y = panLock.yMax;
      repan = true;
    }

    if (repan) {
      panZoomGalaxy.pan({x: x, y: y});
    }
  };

  panZoomGalaxy = svgPanZoom(displayElem, {
    fit: false,
    center: false,
    dblClickZoomEnabled: false,
    zoomScaleSensitivity: 0.025,
    minZoom: 1,
    maxZoom: 2,
    onZoom: function(){
      var pan = panZoomGalaxy.getPan();
      checkPosition(pan.x, pan.y);
    },
    onPan: function(x, y){
      checkPosition(x, y);
    }
  });

I'm interested to hear any opinions on the performance implications of this approach.

@ariutta
Copy link
Collaborator Author

ariutta commented Nov 12, 2014

@bumbu, I don't know of a case where a user would need a function, but I'm OK with having it as an option in the API if you want to implement it.

@ariutta
Copy link
Collaborator Author

ariutta commented Nov 12, 2014

Thanks, @abecks. I'll take a closer look at this.

@bumbu
Copy link
Owner

bumbu commented Nov 25, 2014

I added possibility to cancel panning and zooming from inside of beforePan and beforeZoom.
Both callbacks will receive old state and new state. If callbacks will return false then panning or zooming will be canceled. For panning it is possible to cancel panning on one axis.

Callbacks have access to the public API object through this keyword.

I set up an example in demo/limit-pan.html file on how to keep content visible with a gutter of 100px. It is easy to modify this example to match other gutter values (like 50px or 25%). It is possible to set different vertical and horizontal gutter.

This approach allows many customizations (such as panning in steps requested in #71) while keeping the codebase clean.

Right now changes are in dev branch

@bumbu bumbu closed this as completed in 44708f2 Nov 27, 2014
@faceless2
Copy link

Can I suggest a better option is changing beforePan to return an {x, y} object, the same as is passed in. That makes it much more flexible.

For instance, to cancel the pan just return "oldPan". To accept the pan as requested, return "newPan". To clip at the edges, the X and Y values can be limited to the edges if they're scrolled beyond - for example, an oversimplified example showing how to prevent the top-left scrolling out of view:

beforePan(oldpan, newpan) {
return { x: Math.max(newpan.x, 0), y: Math.max(newpan.y, 0) };
}

The reason for taking this approach is if I grab the SVG and do a great swipe left/right: if I return a boolean I can only cancel the pan, but if I return x and y I can accept the pan but limit it to the bounds of the image.

Edit: In fact, to really improve things, add a "z" parameter for zoom, and roll all of the minzoom/maxzoom/pan limiting functionality into one simple function.

@bumbu
Copy link
Owner

bumbu commented Dec 22, 2014

Hi @faceless2,

It sounds like a reasonable proposal. I was thinking about something like this but I wasn't sure if it is a good approach. The idea sounds promising as it may allow to do such things as pan sensitivity or panning by the limit when user tried to pan over limit. But at the same time it may be a trap as pan events are computed relatively to pan starting point and they do not take in account pan canceling or any modifications at beforePan time.

What do you mean by

add a "z" parameter for zoom, and roll all of the minzoom/maxzoom/pan limiting functionality into one simple function
Can you please more explicit here? Or maybe an example will help me to understand.

Thanks for your feedback.

@bumbu bumbu reopened this Dec 22, 2014
@faceless2
Copy link

Currently you have minZoom and maxZoom variables, you have a beforePan method and a beforeZoom method. It occurs to me that you could replace all of these with a single "beforeChange" method that takes before and after values of x, y and zoom, and returns an object with the same set of values.

You could use this to limit zoom levels or to adjust pan position after a zoom.

Why do this? If I have scrolled to the far right of the SVG bounding box and zoom out, the left and top edge of my image are fixed, which means I get white space to the right when I zoom out. Ideally I would like to pan the image as well at this point to ensure the viewBox is always respected.

An example. This will (well, should - untested) limit the panned area to the viewbox, and limit zoom to between 1 and 4. I'm assuming the SVG is >= in size to the width/height available, which I can do because it's just an example :-)

function(op, np) {
  var s = this.getSizes();
  var x, y, zoom;
  zoom = Math.max(1, Math.min(4, np.z));
  x = Math.min(np.x, Math.max(s.viewBox.x, op.x));
  x = Math.max(x, Math.min(op.x, s.width - ((s.viewBox.x + s.viewBox.width) * zoom)));
  y = Math.min(np.y, Math.max(s.viewBox.y, op.y));
  y = Math.max(y, Math.min(op.y, s.height - ((s.viewBox.y + s.viewBox.height) * zoom)));
  return { x:x, y:y, z:zoom };
},

@bumbu
Copy link
Owner

bumbu commented Jan 5, 2015

I added ability to alter pan from beforePan callback.
Now you can return a object of type {x: 10, y: 20} and pan will be updated to these values.
In order to cancel pan you can pass oldPan.

Currently you have minZoom and maxZoom variables, you have a beforePan method and a beforeZoom method. It occurs to me that you could replace all of these with a single "beforeChange" method that takes before and after values of x, y and zoom, and returns an object with the same set of values.

If this is still a wanted feature then please open a new issue for this.
It may be an idea for version 4, but it has to be discussed.

Thanks

@bumbu bumbu closed this as completed Jan 5, 2015
@hirschferkel
Copy link

hirschferkel commented Jan 29, 2019

I added ability to alter pan from beforePan callback.
Now you can return a object of type {x: 10, y: 20} and pan will be updated to these values.
In order to cancel pan you can pass oldPan.

I made it work to limit the pan using the demo limit-pan.html but this seems to be different to the solution with a callback. Could you explain how one could use the beforePan / oldPan method to limit panning?

Best, hirschferkel

@bumbu
Copy link
Owner

bumbu commented Jan 31, 2019

@hirschferkel you can find more description on home page. Look for the description about beforePan where it's explained what arguments are passed in, and what the return will do. Hope this helps

@hirschferkel
Copy link

@bumbu sorry I had only a sketchy look at it when I asked for help. Initially I used the method already... thank's for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants