最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Using CSS transform scale() to zoom into an element without cropping, maintaining scrolling - Stack Overflow

programmeradmin0浏览0评论

Live example: /

It's possible to use the CSS scale and translate functions to zoom into element.

Take this example, of 4 boxes in a 2x2 grid.

HTML:

<div id="container">
  <div id="zoom-container">
    <div class="box red">A</div>
    <div class="box blue">B</div>
    <div class="box green">C</div>
    <div class="box black">D</div>
  </div>
</div>

CSS:

* { margin: 0; }

body, html { height: 100%; }

#container {
  height: 100%;
  width: 50%;
  margin: 0 auto;
}

#zoom-container {
  height: 100%;
  width: 100%;
  transition: all 0.2s ease-in-out;
}

.box {
  float: left;
  width: 50%;
  height: 50%;
  color: white;
  text-align: center;
  display: block;
}

.red { background: red; }
.blue { background: blue; }
.green { background: green; }
.black { background: black; }

JavaScript:

window.zoomedIn = false;

$(".box").click(function(event) {
  var el = this;
  var zoomContainer = $("#zoom-container");

  if (window.zoomedIn) {
    console.log("resetting zoom");
    zoomContainer.css("transform", "");
    $("#container").css("overflow", "auto");
    window.zoomedIn = false;
  } else {
    console.log("applying zoom");
    var top = el.offsetTop;
    var left = el.offsetLeft - 0.25*zoomContainer[0].clientWidth;

    var translateY = 0.5*zoomContainer[0].clientHeight - top;
    var translateX = 0.5*zoomContainer[0].clientWidth - left;

    $("#container").css("overflow", "scroll");
    zoomContainer.css("transform", "translate(" + 2 * translateX + "px, " + 2 * translateY + "px) scale(2)");
    window.zoomedIn = true;
  }
});

By controlling the value of translateX and translateY, you can change how the zooming works.

The initial rendered view looks something like this:

Clicking on the A box will zoom you in appropriately:

(Note that clicking D at the end is just showing the reset by zooming back out.)

The problem is: zooming to box D will scale the zoom container such that scrolling to the top and left doesn't work, because the contents overflow. The same happens when zooming to boxes B (the left half is cropped) and C (the top half is cropped). Only with A does the content not overflow outside the container.

In similar situations related to scaling (see CSS3 Transform Scale and Container with Overflow), one possible solution is to specify transform-origin: top left (or 0 0). Because of the way the scaling works relative to the top left, the scrolling functionality stays. That doesn't seem to work here though, because it means you're no longer repositioning the contents to be focused on the clicked box (A, B, C or D).

Another possible solution is to add a margin-left and a margin-top to the zoom container, which adds enough space to make up for the overflowed contents. But again: the translate values no longer line up.

So: is there a way to both zoom in on a given element, and overflow with a scroll so that contents aren't cropped?

Update: There's a rough almost-solution by animating scrollTop and scrollLeft, similar to (see the jsfiddle example), but it's not quite a proper solution because it first zooms to the top left, not the intended target. I'm beginning to suspect this isn't actually possible, because it's probably equivalent to asking for scrollLeft to be negative.

Live example: https://jsfiddle.net/b8vLg0ny/

It's possible to use the CSS scale and translate functions to zoom into element.

Take this example, of 4 boxes in a 2x2 grid.

HTML:

<div id="container">
  <div id="zoom-container">
    <div class="box red">A</div>
    <div class="box blue">B</div>
    <div class="box green">C</div>
    <div class="box black">D</div>
  </div>
</div>

CSS:

* { margin: 0; }

body, html { height: 100%; }

#container {
  height: 100%;
  width: 50%;
  margin: 0 auto;
}

#zoom-container {
  height: 100%;
  width: 100%;
  transition: all 0.2s ease-in-out;
}

.box {
  float: left;
  width: 50%;
  height: 50%;
  color: white;
  text-align: center;
  display: block;
}

.red { background: red; }
.blue { background: blue; }
.green { background: green; }
.black { background: black; }

JavaScript:

window.zoomedIn = false;

$(".box").click(function(event) {
  var el = this;
  var zoomContainer = $("#zoom-container");

  if (window.zoomedIn) {
    console.log("resetting zoom");
    zoomContainer.css("transform", "");
    $("#container").css("overflow", "auto");
    window.zoomedIn = false;
  } else {
    console.log("applying zoom");
    var top = el.offsetTop;
    var left = el.offsetLeft - 0.25*zoomContainer[0].clientWidth;

    var translateY = 0.5*zoomContainer[0].clientHeight - top;
    var translateX = 0.5*zoomContainer[0].clientWidth - left;

    $("#container").css("overflow", "scroll");
    zoomContainer.css("transform", "translate(" + 2 * translateX + "px, " + 2 * translateY + "px) scale(2)");
    window.zoomedIn = true;
  }
});

By controlling the value of translateX and translateY, you can change how the zooming works.

The initial rendered view looks something like this:

Clicking on the A box will zoom you in appropriately:

(Note that clicking D at the end is just showing the reset by zooming back out.)

The problem is: zooming to box D will scale the zoom container such that scrolling to the top and left doesn't work, because the contents overflow. The same happens when zooming to boxes B (the left half is cropped) and C (the top half is cropped). Only with A does the content not overflow outside the container.

In similar situations related to scaling (see CSS3 Transform Scale and Container with Overflow), one possible solution is to specify transform-origin: top left (or 0 0). Because of the way the scaling works relative to the top left, the scrolling functionality stays. That doesn't seem to work here though, because it means you're no longer repositioning the contents to be focused on the clicked box (A, B, C or D).

Another possible solution is to add a margin-left and a margin-top to the zoom container, which adds enough space to make up for the overflowed contents. But again: the translate values no longer line up.

So: is there a way to both zoom in on a given element, and overflow with a scroll so that contents aren't cropped?

Update: There's a rough almost-solution by animating scrollTop and scrollLeft, similar to https://stackoverflow.com/a/31406704/528044 (see the jsfiddle example), but it's not quite a proper solution because it first zooms to the top left, not the intended target. I'm beginning to suspect this isn't actually possible, because it's probably equivalent to asking for scrollLeft to be negative.

Share Improve this question edited May 23, 2017 at 11:54 CommunityBot 11 silver badge asked Apr 3, 2016 at 17:10 ap.ap. 1,1752 gold badges11 silver badges18 bronze badges 6
  • If I say, all the boxes gets zoomed in but the one clicked should be in view, right?, and then, by scrolling, one can move to any of the boxes, right? – Asons Commented Apr 3, 2016 at 18:41
  • Clicking a box should zoom in on the clicked box. It should then be possible to scroll to any of the other boxes to bring them into view. – ap. Commented Apr 3, 2016 at 18:53
  • 1 I am working little-by-little with this, got this far, jsfiddle.net/LGSon/b8vLg0ny/5 ... and I might offer a bounty when possible (2 days after posting), to give more attraction to your question as I think it is a good one, and I also have an interest in a similar solution – Asons Commented Apr 4, 2016 at 18:57
  • I've updated my question with a link to stackoverflow.com/a/31406704/528044 which has a rough half-solution but the animation timing is a bit clunky. – ap. Commented Apr 4, 2016 at 19:05
  • There was another similar question I encountered before that you might find helpful: stackoverflow.com/questions/34196639/zooming-in-overflow-scroll My own answer over there can't satisfy all of your requirements though. – AVAVT Commented Apr 7, 2016 at 20:42
 |  Show 1 more comment

3 Answers 3

Reset to default 8 +150

Why not just to reposition the TransformOrigin to 0 0 and to use proper scrollTop/scrollLeft after the animation?

  • https://jsfiddle.net/b8vLg0ny/7/
  • Updated: https://jsfiddle.net/b8vLg0ny/13/

If you do not need the animation, the TransformOrigin can always stays 0 0 and only the scrolling is used to show the box.

To make the animation less jumpy use transition only for transform porperty, otherwise the transform-origin gets animated also. I have edited the example with 4x4 elements, but I think it makes sense to zoom a box completely into view, thats why I changed the zoom level. But if you stay by zoom level 2 and the grid size 15x15 for instance, then with this approach really precise origin should be calculated for transform, and then also the correct scrolling.

Anyway I don't know, if you find this approach useful.

Stack snippet

var zoomedIn = false;
var zoomContainer = $("#zoom-container");

$(".box").click(function(event) {
  var el = this;
  
  if (zoomedIn) {    
    zoomContainer.css({
    	transform: "scale(1)",
      transformOrigin: "0 0"
    });
    zoomContainer.parent().scrollTop(0).scrollLeft(0);
    zoomedIn = false;
    return;
  } 
  zoomedIn = true;
  var $el = $(el);
  animate($el);
  zoomContainer.on('transitionend', function(){
  	zoomContainer.off('transitionend');
  	reposition($el);
  })
});

var COLS = 4, ROWS = 4, 
  	COLS_STEP = 100 / (COLS - 1), ROWS_STEP = 100 / (ROWS - 1),
    ZOOM = 4;
  

function animate($box) {
  var cell = getCell($box);
  var col =  cell.col * COLS_STEP + '%',
      row =  cell.row * ROWS_STEP + '%';
  zoomContainer.parent().css('overflow', 'hidden');
	zoomContainer.css({
    transition: 'transform 0.2s ease-in-out',
  	transform: "scale(" + ZOOM + ")",
    transformOrigin: col + " " + row
  });
}
function reposition($box) {
  zoomContainer.css({
    transition: 'none',
  	transform: "scale(" + ZOOM + ")",
    transformOrigin: '0 0'
  });  
  zoomContainer.parent().css('overflow', 'auto');
  $box.get(0).scrollIntoView();
}
function getCell ($box) {
	var idx = $box.index();
  var col = idx % COLS,
      row =  (idx / ROWS) | 0;
  return { col: col, row: row };
}
* { margin: 0; }

body, html { height: 100%; }

#container {
  height: 100%;
  width: 50%;
  margin: 0 auto;
  overflow: hidden;
}

#zoom-container {
  height: 100%;
  width: 100%;
  will-change: transform;
}

.box {
  float: left;
  width: 25%;
  height: 25%;
  color: white;
  text-align: center;  
}

.red { background: red; }
.blue { background: blue; }
.green { background: green; }
.black { background: black; }
.l { opacity: .3 }
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>

<div id="container">
  <div id="zoom-container">
    <div class="box red">A</div>
    <div class="box blue">B</div>
    <div class="box green">C</div>
    <div class="box black">D</div>

    <div class="box red l">E</div>
    <div class="box blue l">F</div>
    <div class="box green l">G</div>
    <div class="box black l">H</div>

    <div class="box red">I</div>
    <div class="box blue">J</div>
    <div class="box green">K</div>
    <div class="box black">L</div>

    <div class="box red l">M</div>
    <div class="box blue l">N</div>
    <div class="box green l">O</div>
    <div class="box black l">P</div>
  </div>
</div>

I'm answering my own question, since I'm fairly confident that it's actually not possible with the given requirements. At least not without some hackery that would cause problems visually, e.g., jumpy scrolling by animating scrollTop after switching transform-origin to 0, 0 (which removes the cropping by bringing everything back into the container).

I'd love for someone to prove me wrong, but it seems equivalent to asking for scrollLeft = -10, something that MDN will tell you is not possible. ("If set to a value less than 0 [...], scrollLeft is set to 0.")

If, however, it's acceptable to change the UI from scrolling, to zooming and dragging/panning, then it's achievable: https://jsfiddle.net/jegn4x0f/5/

Here's the solution with the same context as my original problem:

HTML:

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>

<button id="zoom-out">Zoom out</button>

<div id="container">
  <div id="inner-container">
    <div id="zoom-container">
      <div class="box red">A</div>
      <div class="box blue">B</div>
      <div class="box green">C</div>
      <div class="box black">D</div>
    </div>
  </div>
</div>

JavaScript:

//
// credit for the approach goes to
//
//   https://stackoverflow.com/questions/35252249/move-drag-pan-and-zoom-object-image-or-div-in-pure-js#comment58224460_35253567
//
// and the corresponding example:
//
//  https://jsfiddle.net/j8kLz6wm/1/
//

// in a real-world setting, you
// wouldn't keep this information
// on window. this is just for
// the demonstration.
window.zoomedIn = false;

// stores the initial translate values after clicking on a box
window.translateY = null;
window.translateX = null;

// stores the incremental translate values based on
// applying the initial translate values + delta
window.lastTranslateY = null;
window.lastTranslateX = null;

// cursor position relative to the container, at
// the time the drag started
window.dragStartX = null;
window.dragStartY = null;

var handleDragStart = function(element, xCursor, yCursor) {
  window.dragStartX = xCursor - element.offsetLeft;
  window.dragStartY = yCursor - element.offsetTop;

  // disable transition animations, since we're starting a drag
  $("#zoom-container").css("transition", "none");
};

var handleDragEnd = function() {
  window.dragStartX = null;
  window.dragStartY = null;
  // remove the individual element's styling for transitions
  // which brings back the stylesheet's default of animating.
  $("#zoom-container").css("transition", "");

  // keep track of the translate values we arrived at
  window.translateY = window.lastTranslateY;
  window.translateX = window.lastTranslateX;
};

var handleDragMove = function(xCursor, yCursor) {
  var deltaX = xCursor - window.dragStartX;
  var deltaY = yCursor - window.dragStartY;

  var translateY = window.translateY + (deltaY / 2);
  // the subtracted value here is to keep the letter in the center
  var translateX = window.translateX + (deltaX / 2) - (0.25 * $("#inner-container")[0].clientWidth);

  // fudge factor, probably because of percentage
  // width/height problems. couldn't really trace down
  // the underlying cause. hopefully the general approach
  // is clear, though.
  translateY -= 9;
  translateX -= 4;

  var innerContainer = $("#inner-container")[0];

  // cap all values to prevent infinity scrolling off the page
  if (translateY > 0.5 * innerContainer.clientHeight) {
    translateY = 0.5 * innerContainer.clientHeight;
  }

  if (translateX > 0.5 * innerContainer.clientWidth) {
    translateX = 0.5 * innerContainer.clientWidth;
  }

  if (translateY < -0.5 * innerContainer.clientHeight) {
    translateY = -0.5 * innerContainer.clientHeight;
  }

  if (translateX < -0.5 * innerContainer.clientWidth) {
    translateX = -0.5 * innerContainer.clientWidth;
  }

  // update the zoom container's translate values
  // based on the original + delta, capped to the
  // container's width and height.
  $("#zoom-container").css("transform", "translate(" + (2*translateX) + "px, " + (2*translateY) + "px) scale(2)");

  // keep track of the updated values for the next
  // touchmove event.
  window.lastTranslateX = translateX;
  window.lastTranslateY = translateY;
};

// Drag start -- touch version
$("#container").on("touchstart", function(event) {
  if (!window.zoomedIn) {
    return true;
  }

  var xCursor = event.originalEvent.changedTouches[0].clientX;
  var yCursor = event.originalEvent.changedTouches[0].clientY;

  handleDragStart(this, xCursor, yCursor);
});

// Drag start -- mouse version
$("#container").on("mousedown", function(event) {
  if (!window.zoomedIn) {
    return true;
  }

  var xCursor = event.clientX;
  var yCursor = event.clientY;

  handleDragStart(this, xCursor, yCursor);
});

// Drag end -- touch version
$("#inner-container").on("touchend", function(event) {
  if (!window.zoomedIn) {
    return true;
  }

  handleDragEnd();
});

// Drag end -- mouse version
$("#inner-container").on("mouseup", function(event) {
  if (!window.zoomedIn) {
    return true;
  }

  handleDragEnd();
});

// Drag move -- touch version
$("#inner-container").on("touchmove", function(event) {
  // prevent pull-to-refresh. could be smarter by checking
  // if the page's scroll y-offset is 0, and even smarter
  // by checking if we're pulling down, not up.
  event.preventDefault();

  if (!window.zoomedIn) {
    return true;
  }

  var xCursor = event.originalEvent.changedTouches[0].clientX;
  var yCursor = event.originalEvent.changedTouches[0].clientY;

  handleDragMove(xCursor, yCursor);
});

// Drag move -- click version
$("#inner-container").on("mousemove", function(event) {
  // prevent pull-to-refresh. could be smarter by checking
  // if the page's scroll y-offset is 0, and even smarter
  // by checking if we're pulling down, not up.
  event.preventDefault();

  // if we aren't dragging from anywhere, don't move
  if (!window.zoomedIn || !window.dragStartX) {
    return true;
  }

  var xCursor = event.clientX;
  var yCursor = event.clientY;

  handleDragMove(xCursor, yCursor);
});

var zoomInTo = function(element) {
  console.log("applying zoom");

  var top = element.offsetTop;
  // the subtracted value here is to keep the letter in the center
  var left = element.offsetLeft - (0.25 * $("#inner-container")[0].clientWidth);

  var translateY = 0.5 * $("#zoom-container")[0].clientHeight - top;
  var translateX = 0.5 * $("#zoom-container")[0].clientWidth - left;

  $("#container").css("overflow", "scroll");
  $("#zoom-container").css("transform", "translate(" + (2*translateX) + "px, " + (2*translateY) + "px) scale(2)");
  window.translateY = translateY;
  window.translateX = translateX;

  window.zoomedIn = true;
}

var zoomOut = function() {
  console.log("resetting zoom");

  window.zoomedIn = false;
  $("#zoom-container").css("transform", "");
  $("#zoom-container").css("transition", "");
  window.dragStartX = null;
  window.dragStartY = null;
  window.dragMoveJustHappened = null;
  window.translateY = window.lastTranslateY;
  window.translateX = window.lastTranslateX;
  window.lastTranslateX = null;
  window.lastTranslateY = null;
}

$(".box").click(function(event) {
  var element = this;
  var zoomContainer = $("#zoom-container");

  if (!window.zoomedIn) {
    zoomInTo(element);
  }
});

$("#zoom-out").click(function(event) {
  zoomOut();
});

CSS:

* {
  margin: 0;
}

body,
html {
  height: 100%;
}

#container {
  height: 100%;
  width: 50%;
  margin: 0 auto;
}

#inner-container {
  width: 100%;
  height: 100%;
}

#zoom-container {
  height: 100%;
  width: 100%;
  transition: transform 0.2s ease-in-out;
}

.box {
  float: left;
  width: 50%;
  height: 50%;
  color: white;
  text-align: center;
  display: block;
}

.red {
  background: red;
}

.blue {
  background: blue;
}

.green {
  background: green;
}

.black {
  background: black;
}

I pieced this together from another question (Move (drag/pan) and zoom object (image or div) in pure js), where the width and height are being changed. That doesn't quite apply in my case, because I need to zoom into a specific element on the page (with a lot boxes than in a 2x2 grid). The solution from that question (https://jsfiddle.net/j8kLz6wm/1/) shows the basic approach in pure JavaScript. If you have jQuery available, you can probably just use jquery.panzoom.

Update

I got stuck on scroll bars not showing all the time, so I need to investigating that part, so that code is commented out and instead I use a delay to move the clicked box into view.

Here is my fiddle demo, which I use to play with, to figure out how to solve the scroll bar issue.

Side note: In a comment made by @AVAVT, I would like to link to his post here, as that might help someone else, which I find as an interesting alternative in some cases.

(function(zoomed) {
  
  $(".box").click(function(event) {
    
    var el = this, elp = el.parentElement;
    
    if (zoomed) {
      zoomed = false;
      $("#zoom-container").css({'transform': ''});
      
    } else {
      zoomed = true;
      /*  this zooms correct but show 1 or none scroll for B,C,D so need to figure out why
      
      var tro = (Math.abs(elp.offsetTop - el.offsetTop) > 0) ? 'bottom' : 'top';
      tro += (Math.abs(elp.offsetLeft - el.offsetLeft) > 0) ? ' right' : ' left';
      $("#zoom-container").css({'transform-origin': tro, 'transform': 'scale(2)'});
      */
      
      $("#zoom-container").css({'transform-origin': '0 0', 'transform': 'scale(2)'});
      /* delay needed before scroll into view */      
      setTimeout(function() {
        el.scrollIntoView();
      },250);
    }    
  });
})();
* { margin: 0; }

body, html { height: 100%; }

#container {
  height: 100%;
  width: 50%;
  overflow: auto;
  margin: 0 auto;
}

#zoom-container {
  height: 100%;
  width: 100%;
  transition: all 0.2s ease-in-out;
}

.box {
  float: left;
  width: 50%;
  height: 50%;
  color: white;
  text-align: center;
  display: block;
}

.red {
  background: red; 
}
.blue {
  background: blue;
}
.green {
  background: green;
}
.black {
  background: black;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<div id="container">
  <div id="zoom-container">
    <div class="box red">A</div>
    <div class="box blue">B</div>
    <div class="box green">C</div>
    <div class="box black">D</div>
  </div>
</div>

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论