By not thoroughly thinking about how the CSS background property works with percentages, I faced a few problems when using it on a simple tool I worked on recently.
The aim was to create a simple tool that allows you to move (position) an image within a container.
These are the goals we set out to achieve:
- You should be able to drag the image around the container by clicking and moving your mouse around.
- You should be able to resize the image within the container (zooming in and out).
- The image should be on repeat, so if the width isn’t as big as the container, it will just repeat to fill this out.
- Make it responsive – keep the same position when the browser window is resized.
Based on those simple requirements, I created this:
See the Pen Draggable Background by Denis Sellu (@denissellu) on CodePen.
These are the core decisions I made to achieve this:
- To make the image a background of the container. This allows us to easily achieve points 2 and 3 on the list of requirements.
- To have a simple range slider to control the percentage of the background size, which will act like a zoom (in and out of the background).
- I used background position in percentages to achieve point 4.
How it Works
All the needed elements are created when the class is initialised:
imageControl.init
container: '.container'
containerSize: '500px'
backgroundPattern: 'http://i.imgur.com/niFagYy.jpg'
An event to detect a mousedown and touchstart (touch screen) to let us know when the mouse is clicked and being moved around within the container:
imageContainer.on 'mousedown touchstart', (e) ->
We record the position at which you originally clicked:
mousedown =
x: e.originalEvent.pageX || e.originalEvent.touches[0].pageX
y: e.originalEvent.pageY || e.originalEvent.touches[0].pageY
Now we need to know when you start moving your mouse around. Since there is a possibility of moving outside the main container, we will bind the event to the page document:
$(document).on 'mousemove touchmove', (e) ->
Every time you move your cursor, we record the new position:
mousepos =
x: e.originalEvent.pageX || e.originalEvent.changedTouches[0].pageX || mousedown.x
y: e.originalEvent.pageY || e.originalEvent.changedTouches[0].pageY || mousedown.y
Then we check it against the original position where you clicked just to make sure we are not doing needless calculations. Now that we know where you originally clicked and where your cursor is currently, we can calculate the distance you have moved by subtracting the mousespos
from mousedown
. Since the result of the distance will be in pixels, we need to convert it to percentages relative to that of the container:
if mousedown != mousepos
xpos = (100 * (mousepos.x - mousedown.x)) / patternBackground.width()
ypos = (100 * (mousepos.y - mousedown.y)) / patternBackground.width()
The last thing to do now that we have our percentage is to update the background position:
patternBackground.css
'background-position': "#{xpos}% #{ypos}%"
Problem
The problem is that if you zoom in on the image and try dragging it around, you will see that it moves a lot slower than when it is zoomed out.
Attempt at Solution
- I could just use pixels which would solve the problem, but it wouldn’t match up with point 4 on my aim. Also, it would just be avoiding the issue altogether – that’s no fun.
- The most appropriate fix I could come up with was to use a coefficient. This needs to change as the image gets larger – the only variable that should be changed as the image gets better is the zoom.
Using Zoom as a coefficient
After I calculated the move percent(xpos
, ypos
), I multiplied it by the zoom. Unfortunately, this didn’t work as expected. The speed at which the background moved at the initial zoom level became faster; however, it still slowed down to an unusable level as I zoomed in. The only reason this wouldn’t work is because background position in percentages didn’t work as expected.
Understanding Background position
I read the spec from W3 to see how background percentages actually worked. The spec defines background position percentage as: refer to size of background positioning area minus size of background image
. The vital information I was missing was the fact that background position is calculated by subtracting the background image from its’ parent container and whatever space is left will determine what the image will move in. This is why it became ‘slower’ as you zoomed in, because the space remaining within the parent container of the image isn’t large.
Solution
With my new found understanding of how background percentages work, I decided to come up with an equation for calculating my new movement percentage.
Defining the problem with maths
\[\begin{aligned}
z & = Zoom Level \\
c & = Container Size \\
a & = Move Percentage \\
p & = Moved in Pixel \\
x & = New Movement Percentage \\
\end{aligned} \]
To calculate the movement in pixel with the zoom as the coefficient:
$$
p = c \times (1 – z) \times a
$$
I found that I still didn’t get the expected results with this, so I divided everything by the container size and introduced a constant of 70% (the level at which the background moved smoothly). This meant that whatever zoom level you were at, the background would move at the same speed it moved at 70%.
$$
x = {c \times ({\frac {0.7}{1 – z}}) \times a \over c }
$$
I cleaned up a bit by removing the container size, as it canceled it self out:
$$
x = {(\frac{0.7}{1 – z}) \times a}
$$
movePercentage =
x: ( 100 * (mousepos.x - mousedown.x)) / (patternBackgroundWidth)
y: ( 100 * (mousepos.y - mousedown.y)) / (patternBackgroundWidth)
actualMovePercentage =
x: (((0.7 / ( 1 - (zoomLevel/100))) * movePercentage.x ))
y: (((0.7 / ( 1 - (zoomLevel/100))) * movePercentage.y ))
Demo
This is the completed implementation:
See the Pen Draggable Background by Denis Sellu (@denissellu) on CodePen.
Future Improvement
This solution works really well – it ticks all the boxes of what we were after. Unfortunately, as we have been using it, we have discovered a new problem. If you zoom in to say 98% and move by 10%, this will of course be translated to the thousand, so you may end up with a new background position of about 3000% by 3000%. That’s fine at that size, but if you were to zoom back out, 3000% by 3000% of a smaller image will be extremely out of bounds for the container. A solution I’ve come up with, but haven’t implemented (yet) is to recalculate the background position as you resize using the same formula.