Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CSS/Jquery Smooth Background Movement with Mouse

Tags:

jquery

css

I'm trying to get a smoothly panned zoomed image from mouse movement over a thumbnail.

I've tried all types of transition durations, custom tweening functions, jquery animate and none of it is getting a nice result.

When a thumbnail is small then the pixel jumps over it are large enough to make the image stagger.

Jquery animations start stacking, css transition changes abruptly, custom functions, no idea how to change direction easily.

CodePen

const ZOOM_LEVEL = 2;

$(document).ready(function() {
  $(".thumb").mouseenter(enter);
  $(".thumb").mouseleave(leave);
  $('.thumb').mousemove(zoom);
});

function zoom(event) {
  const p = calculateZoomOverlay({x: event.pageX, y: event.pageY}, $(event.target));
  moveCursorOverlay(p.left, p.top);
  movePreviewBackground(p.offsetX, p.offsetY);
}

function calculateZoomOverlay(mouse, thumb) {
  let t = thumb.position();
  t.width = thumb.width();
  t.height = thumb.height();

  let z = {}; // Zoom overlay
  z.width = t.width / ZOOM_LEVEL;
  z.height = t.height / ZOOM_LEVEL;
  z.top = mouse.y - z.height / 2;
  z.left = mouse.x - z.width / 2;
  
  // Bounce off boundary
  if (z.top < t.top) z.top = t.top;
  if (z.left < t.left) z.left = t.left;
  if (z.top + z.height > t.top + t.height) z.top = t.top + t.height - z.height;
  if (z.left + z.width > t.left + t.width) z.left = t.left + t.width - z.width;

  z.offsetX = (z.left - t.left) / z.width * 100;
  z.offsetY = (z.top - t.top) / z.height * 100;

  return z;
}

function moveCursorOverlay(left, top) {
   $('.cursor-overlay').css({
    top: top,
    left: left
  });
}

function movePreviewBackground(offsetX, offsetY) {
  $('.preview').css({
    'background-position': offsetX + '% ' + offsetY + '%'
  });
}

function enter() {
  // Setup preview image
  const imageUrl = $(this).attr('src');
  const backgroundWidth = $('.preview').width() * ZOOM_LEVEL;
  $('.preview').css({
    'background-image': `url(${imageUrl})`,
    'background-size': `${backgroundWidth} auto`
  });
  $('.preview').show();

  $('.cursor-overlay').width($(this).width() / ZOOM_LEVEL);
  $('.cursor-overlay').height($(this).height() / ZOOM_LEVEL);
  $('.cursor-overlay').show();
}

function leave() {
  $('.preview').hide();
  $('.cursor-overlay').hide();
}
.image-container {
  padding: 5px;
  display: flex;
  flex-direction: row;
}

.thumbnail-container {
  display: flex;
  flex-direction: column;
}

.thumb {
  margin-bottom: 5px;
  width: 80px;
  height: 50px;
}

.thumb:hover {
  -moz-box-shadow: 0 0 5px orange;
  -webkit-box-shadow: 0 0 5px orange;
  box-shadow: 0 0 5px orange;
}

.preview {
  display: none;
  margin-left: 15px;
  width: 320px;
  height: 200px;
  border: 3px solid orange;
}

.cursor-overlay {
  display: none;
  background-color: rgba(0, 150, 50, 0.5);
  position: fixed;
  pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="image-container">
  <div class="thumbnail-container">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/sbrYaxH.jpg">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/2PpkoRZ.jpg">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/3lOTtJV.jpg">
  </div>

  <div class="cursor-overlay"></div>
  <div class="preview"></div>
</div>
like image 329
Joshua Jenkins Avatar asked Dec 03 '19 05:12

Joshua Jenkins


3 Answers

This is a case where in my opinion CSS transitions do not cut it. They are not generally great for interactive animations like this. Many animation libraries will also not get the job done.

To achieve this kind of interactive animation I like to use the following equation:

loc += (destination - location) / 2

This is an easing equation - great for interactive animation. It can be likened to Zeno's Dichotomy paradox

Here we get the difference between a destination and a location on a single axis, we half that difference and add it to the current location.

When we apply this to your code we end up with:

 z.top += (dy - z.top) / SMOOTH;
 z.left += (dx - z.left) / SMOOTH;

For a more smooth animation we have a SMOOTH constant which I set to 8. Have a look at how it ends up looking:

const ZOOM_LEVEL = 2;
const SMOOTH = 8; // how much smoothness/delay do you want on the animation

let thumb = $('.thumb');
$(document).ready(function() {
  thumb.mouseenter(enter);
  thumb.mouseleave(leave);
  thumb.mousemove(move);
});

let mouseX = 0;
let mouseY = 0; 
let z = { top: 0, left: 0 }; // Zoom overlay
let dx;
let dy;
function move(event) {
  mouseX = event.pageX;
  mouseY = event.pageY;
  thumb = $(event.target);
}

function loop() {
  const p = calculateZoomOverlay({x: mouseX, y: mouseY});
  moveCursorOverlay(p.left, p.top);
  movePreviewBackground(p.offsetX, p.offsetY);
  window.requestAnimationFrame(loop);
}
loop();

function calculateZoomOverlay(mouse) {
  let t = thumb.position();
  t.width = thumb.width();
  t.height = thumb.height();

  
  z.width = t.width / ZOOM_LEVEL;
  z.height = t.height / ZOOM_LEVEL;
  dy = mouse.y - z.height / 2;
  dx = mouse.x - z.width / 2;
  
  z.top += (dy - z.top) / SMOOTH;
  z.left += (dx - z.left) / SMOOTH;
  
  // Bounce off boundary
  if (z.top < t.top) z.top = t.top;
  if (z.left < t.left) z.left = t.left;
  if (z.top + z.height > t.top + t.height) z.top = t.top + t.height - z.height;
  if (z.left + z.width > t.left + t.width) z.left = t.left + t.width - z.width;

  z.offsetX = (z.left - t.left) / z.width * 100;
  z.offsetY = (z.top - t.top) / z.height * 100;

  return z;
}

function moveCursorOverlay(left, top) {
   $('.cursor-overlay').css({
    top: top,
    left: left
  });
}

function movePreviewBackground(offsetX, offsetY) {
  $('.preview').css({
    'background-position': offsetX + '% ' + offsetY + '%'
  });
}

function enter() {
  // Setup preview image
  const imageUrl = $(this).attr('src');
  const backgroundWidth = $('.preview').width() * ZOOM_LEVEL;
  $('.preview').css({
    'background-image': `url(${imageUrl})`,
    'background-size': `${backgroundWidth} auto`
  });
  $('.preview').show();

  $('.cursor-overlay').width($(this).width() / ZOOM_LEVEL);
  $('.cursor-overlay').height($(this).height() / ZOOM_LEVEL);
  $('.cursor-overlay').show();
}

function leave() {
  $('.preview').hide();
  $('.cursor-overlay').hide();
}
.image-container {
  padding: 5px;
  display: flex;
  flex-direction: row;
}

.thumbnail-container {
  display: flex;
  flex-direction: column;
}

.thumb {
  margin-bottom: 5px;
  width: 80px;
  height: 50px;
}

.thumb:hover {
  -moz-box-shadow: 0 0 5px orange;
  -webkit-box-shadow: 0 0 5px orange;
  box-shadow: 0 0 5px orange;
}

.preview {
  display: none;
  margin-left: 15px;
  width: 320px;
  height: 200px;
  border: 3px solid orange;
}

.cursor-overlay {
  display: none;
  background-color: rgba(0, 150, 50, 0.5);
  position: fixed;
  pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="image-container">
  <div class="thumbnail-container">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/sbrYaxH.jpg">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/2PpkoRZ.jpg">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/3lOTtJV.jpg">
  </div>

  <div class="cursor-overlay"></div>
  <div class="preview"></div>
</div>

You can adjust SMOOTH to your liking.

The structure of the code has changed slightly. Rather than performing the animation on mouse move... we now use a requestAnimationFrame to continuously calculate our animation and respond to changes of the variables mouseX/Y. This prevents abrupt stops in the animation when the user stops moving their mouse.

An optimization could be made to end the calculations if both the z.top and z.left values are very close to or equal to dx and dy. Probably not necessary unless you were dealing with many more calculations.

like image 156
Zevan Avatar answered Oct 18 '22 22:10

Zevan


You could give transition property

<style>
.preview {
   display: none;
   margin-left: 15px;
   width: 640px;
   height: 400px;
   border: 3px solid orange;
   -webkit-transition: all .25s ease-out;
   -moz-transition: all .25s ease-out;
   transition: all .25s ease-out;
}
</style>

const ZOOM_LEVEL = 2;

$(document).ready(function() {
  $(".thumb").mouseenter(enter);
  $(".thumb").mouseleave(leave);
  $('.thumb').mousemove(zoom);
});

function zoom(event) {
  const p = calculateZoomOverlay({x: event.pageX, y: event.pageY}, $(event.target));
  moveCursorOverlay(p.left, p.top);
  movePreviewBackground(p.offsetX, p.offsetY);
}

function calculateZoomOverlay(mouse, thumb) {
  let t = thumb.position();
  t.width = thumb.width();
  t.height = thumb.height();

  let z = {}; // Zoom overlay
  z.width = t.width / ZOOM_LEVEL;
  z.height = t.height / ZOOM_LEVEL;
  z.top = mouse.y - z.height / 2;
  z.left = mouse.x - z.width / 2;
  
  // Bounce off boundary
  if (z.top < t.top) z.top = t.top;
  if (z.left < t.left) z.left = t.left;
  if (z.top + z.height > t.top + t.height) z.top = t.top + t.height - z.height;
  if (z.left + z.width > t.left + t.width) z.left = t.left + t.width - z.width;

  z.offsetX = (z.left - t.left) / z.width * 100;
  z.offsetY = (z.top - t.top) / z.height * 100;

  return z;
}

function moveCursorOverlay(left, top) {
   $('.cursor-overlay').css({
    top: top,
    left: left
  });
}

function movePreviewBackground(offsetX, offsetY) {
  $('.preview').css({
    'background-position': offsetX + '% ' + offsetY + '%'
  });
}

function enter() {
  // Setup preview image
  const imageUrl = $(this).attr('src');
  const backgroundWidth = $('.preview').width() * ZOOM_LEVEL;
  $('.preview').css({
    'background-image': `url(${imageUrl})`,
    'background-size': `${backgroundWidth} auto`
  });
  $('.preview').show();

  $('.cursor-overlay').width($(this).width() / ZOOM_LEVEL);
  $('.cursor-overlay').height($(this).height() / ZOOM_LEVEL);
  $('.cursor-overlay').show();
}

function leave() {
  $('.preview').hide();
  $('.cursor-overlay').hide();
}
.image-container {
  padding: 5px;
  display: flex;
  flex-direction: row;
}

.thumbnail-container {
  display: flex;
  flex-direction: column;
}

.thumb {
  margin-bottom: 5px;
  width: 80px;
  height: 50px;
}

.thumb:hover {
  -moz-box-shadow: 0 0 5px orange;
  -webkit-box-shadow: 0 0 5px orange;
  box-shadow: 0 0 5px orange;
}

.preview {
  display: none;
  margin-left: 15px;
  width: 640px;
  height: 400px;
  border: 3px solid orange;
}

.cursor-overlay {
  display: none;
  background-color: rgba(0, 150, 50, 0.5);
  position: fixed;
  pointer-events: none;
}
.preview {
  display: none;
  margin-left: 15px;
  width: 640px;
  height: 400px;
  border: 3px solid orange;
  transition: background-position 0.1s ease-out;
}

.cursor-overlay {
  display: none;
  background-color: rgba(0, 150, 50, 0.5);
  position: fixed;
  pointer-events: none;
  transition-property: top, left;
  transition-duration: 0.1s;
  transition-timing-function: ease-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="image-container">
  <div class="thumbnail-container">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/sbrYaxH.jpg">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/2PpkoRZ.jpg">
    <img class="thumb" alt="thumbnail" src="https://i.imgur.com/3lOTtJV.jpg">
  </div>

  <div class="cursor-overlay"></div>
  <div class="preview"></div>
</div>
like image 4
Krishna Savani Avatar answered Oct 18 '22 21:10

Krishna Savani


https://github.com/hnprashanth/spree-magiczoomplus Look at this I hope it can help

like image 2
elias sharafi Avatar answered Oct 18 '22 22:10

elias sharafi