CSS/Jquery Smooth Background Movement with Mouse
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.
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>