Использование CSS transform scale() для увеличения элемента без обрезки с сохранением прокрутки

Живой пример: https://jsfiddle.net/b8vLg0ny/

Можно использовать функции CSS scale и translate для увеличения элемента.

Возьмем этот пример из 4 ящиков в сетке 2x2.

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;
  }
});

Управляя значением translateX и translateY, вы можете изменить способ масштабирования.

Начальный визуализированный вид выглядит примерно так:

начальный визуализированный вид

Нажав на поле A, вы увеличите масштаб соответствующим образом:

увеличение до A, а затем уменьшение

(Обратите внимание, что щелчок D в конце просто показывает сброс путем уменьшения масштаба.)

Проблема: масштабирование до поля D масштабирует контейнер масштабирования таким образом, что прокрутка вверх и влево не работает, поскольку содержимое переполняется. То же самое происходит при приближении к полям B (обрезается левая половина) и C (обрезается верхняя половина). Только с A содержимое не выходит за пределы контейнера.

В подобных ситуациях, связанных с масштабированием (см. CSS3 Transform Scale and Container with Overflow), одним из возможных решений является указание transform-origin: top left (или 0 0). Из-за того, как работает масштабирование относительно верхнего левого угла, функция прокрутки остается. Однако здесь это не работает, потому что это означает, что вы больше не перемещаете содержимое, чтобы сфокусироваться на поле, по которому щелкнули (A, B, C или D).

Другое возможное решение — добавить margin-left и margin-top в контейнер масштабирования, что добавит достаточно места, чтобы компенсировать переполненное содержимое. Но опять же: значения перевода больше не совпадают.

Итак: есть ли способ оба увеличить данный элемент, и переполниться прокруткой, чтобы содержимое не обрезалось?

Обновление: есть грубое почти решение путем анимации scrollTop и scrollLeft, похожее на https://stackoverflow.com/a/31406704/528044 (см. пример jsfiddle), но это не совсем правильное решение, потому что сначала оно приближается к левому верхнему углу, а не к намеченной цели. Я начинаю подозревать, что на самом деле это невозможно, потому что это, вероятно, равносильно тому, чтобы просить, чтобы scrollLeft было отрицательным.


person Adam Prescott    schedule 03.04.2016    source источник
comment
Если я скажу, что все поля будут увеличены, но тот, на который нажали, должен быть в поле зрения, верно?, а затем, прокручивая, можно перейти к любому из полей, верно?   -  person Ason    schedule 03.04.2016
comment
Щелчок по полю должен увеличить масштаб окна, по которому щелкнули. Затем должна быть возможность прокрутить до любого из других полей, чтобы отобразить их.   -  person Adam Prescott    schedule 03.04.2016
comment
Я работаю с этим понемногу, зашел так далеко, jsfiddle.net/LGSon/b8vLg0ny/ 5 ... и я мог бы предложить вознаграждение, когда это возможно (через 2 дня после публикации), чтобы сделать ваш вопрос более привлекательным, поскольку я думаю, что это хороший вопрос, и я также заинтересован в подобном решении.   -  person Ason    schedule 04.04.2016
comment
Я обновил свой вопрос ссылкой на stackoverflow.com/a/31406704/528044, который имеет грубое половинное решение но время анимации немного неуклюже.   -  person Adam Prescott    schedule 04.04.2016
comment
Раньше я сталкивался с другим подобным вопросом, который может оказаться полезным: stackoverflow. com/questions/34196639/zooming-in-overflow-scroll Однако мой собственный ответ не может удовлетворить все ваши требования.   -  person AVAVT    schedule 07.04.2016
comment
@AVAVT Надеюсь, вы не возражаете, что я добавил ссылку на этот пост в свой ответ. Я нахожу это интересным, и поскольку комментарий может исчезнуть, теперь эта ссылка не будет   -  person Ason    schedule 10.04.2016


Ответы (3)


Почему бы просто не изменить положение TransformOrigin на 0 0 и не использовать правильный scrollTop/scrollLeft после анимации?

Если вам не нужна анимация, TransformOrigin может всегда оставаться 0 0, а для отображения окна используется только прокрутка.

Чтобы сделать анимацию менее скачкообразной, используйте переход только для transform порперти, иначе transform-origin тоже анимируется. Я отредактировал пример с 4x4 элементами, но я думаю, что имеет смысл полностью увеличить масштаб окна, поэтому я изменил уровень масштабирования. Но если вы останетесь на уровне масштабирования 2 и размере сетки, например, 15x15, то при таком подходе нужно вычислить действительно точное начало координат для преобразования, а затем и правильную прокрутку.

В любом случае, я не знаю, найдете ли вы этот подход полезным.

Фрагмент стека

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>

person tenbits    schedule 05.04.2016
comment
В основном потому, что всякий раз, когда я пытался это сделать, он, кажется, ломался для больших сеток (скажем, 10x10 или 15x15). Например, это кажется мне очень нервным, когда я щелкаю поля в правом нижнем углу: jsfiddle.net/b8vLg0ny/9 (хотя я допускаю, что мог что-то упустить в JS, где я не обновил достаточное количество значений). Анимация, которая приближается к 0 0, не синхронизируется с анимацией прокрутки. - person Adam Prescott; 05.04.2016
comment
@AdamPrescott После обновления tenbits он теперь работает так, как вам хотелось бы? ... Спрашиваю, потому что не вижу смысла в дальнейшем улучшении моего, так как этот раз великолепен. - person Ason; 10.04.2016
comment
Это похоже на лучшее возможное решение! - person Adam Prescott; 10.04.2016

Я отвечаю на свой вопрос, так как я вполне уверен, что это на самом деле невозможно с заданными требованиями. По крайней мере, не без некоторых хакерских приемов, которые вызовут визуальные проблемы, например, скачкообразная прокрутка путем анимации scrollTop после переключения transform-origin на 0, 0 (что удаляет обрезку, возвращая все обратно в контейнер).

Я бы хотел, чтобы кто-нибудь доказал, что я не прав, но это похоже на запрос scrollLeft = -10, что-то, что MDN сообщит вам, что это невозможно. («Если установлено значение меньше 0 [...], для scrollLeft установлено значение 0».)

Однако если приемлемо изменить пользовательский интерфейс с прокрутки на масштабирование и перетаскивание/панорамирование, то это достижимо: https://jsfiddle.net/jegn4x0f/5/

Вот решение с тем же контекстом, что и моя исходная проблема:

масштаб-панорамирование

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;
}

Я собрал это из другого вопроса (Перемещайте (перетаскивайте/панорамируйте) и масштабируйте объект (изображение или div) в чистом js), где width и height изменяются. Это не совсем применимо в моем случае, потому что мне нужно увеличить определенный элемент на странице (с большим количеством полей, чем в сетке 2x2). Решение этого вопроса (https://jsfiddle.net/j8kLz6wm/1/) показывает базовый подход в чистом JavaScript. Если у вас есть jQuery, вы, вероятно, можете просто использовать jquery.panzoom.

person Adam Prescott    schedule 04.04.2016

Обновлять

Я застрял на полосах прокрутки, которые не отображаются все время, поэтому мне нужно исследовать эту часть, чтобы этот код был закомментирован, и вместо этого я использую задержку, чтобы переместить выбранное поле в поле зрения.

Вот моя демонстрация скрипки, с которой я играю, чтобы понять, как решить проблема с полосой прокрутки.

Примечание: в комментарии @AVAVT я хотел бы дать ссылку на его сообщение здесь, так как это может помочь кому-то еще, что я считаю интересной альтернативой в некоторых случаях.

(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>

person Ason    schedule 03.04.2016
comment
Это не совсем работает. Сравните нажатие D в моем исходном примере с нажатием D в этом. Они не выровнены должным образом, поэтому позиции выключены. Аналогично с B: он смещен на половину ширины представления. - person Adam Prescott; 03.04.2016
comment
@AdamPrescott Да, вам придется настроить это, я этого не делал, просто показал, как получить полосы прокрутки и масштабирование. - person Ason; 03.04.2016
comment
Конечно. Он масштабирует, но не в ту часть, что здесь является основной проблемой. - person Adam Prescott; 03.04.2016