CSS cursor zoom-in/out
For Chrome you have to use the prefixed-property-value:
.button.zoom-in {
...
cursor: -webkit-zoom-in;
cursor: zoom-in;
}
Here is a jsfiddle.
Zoom to cursor without canvas in javascript
Eventually I figured it out myself, although only by looking at existing solutions. Here is the JSFiddle that contains only the essentials.
The idea is to first set transform-origin: 0 0
. This makes sure that, upon zooming, the image expands down and right, instead of distributing the increase in width over all four sides. Note that it does not reposition the image, it just changes the origin for all transformations.
Additionally, this JSFiddle assumes that the top and left margins of the image are aligned with the top and left margins of the container element. If the image should be repositioned before zooming occurs, this should be done through transform: translate()
and the translateX
and translateY
values need to be updated accordingly.
The heart of the logic is this:
// Track the percentage change between the old
// and the new scale of the image
const ratio = 1 - nextScale / currentScale
// get the current mouse offset
const {
clientX,
clientY
} = event
// The += here is extremely important!
// The new 2D translation values are derived from the difference
// between mouse cursor position and current (!) 2D translation.
// So where is the mouse cursor relative to the translated image
// This difference is then adjusted by the % change of the scaling
translateX += (clientX - translateX) * ratio
translateY += (clientY - translateY) * ratio
/*
This would work for the first wheel scroll. But afterwards, the
image will not be translated enough to offset the zooming because
we're not taking into account the existing translation
translateX += (clientX - translateX) * ratio
translateY += (clientY - translateY) * ratio
*/
So to summarize the required steps:
- Calculate the next scale
- Calculate the current mouse offset relative to the translated image
- Adjust the mouse offset for the change in scaling, e.g.,
const percentChange = 1 - nextScale / currentScale
- Add the adjusted mouse offset to the existing values for
translate()
- Apply the transformation (scaling and the translation)
The linked JSFiddle also includes Lodash and transition: transform 330ms ease-in-out;
to make the scrolling a little smoother and not affect browser performance too much.
Zoom/scale at mouse position
Use the canvas for zoomable content
Zooming and panning elements is very problematic. It can be done but the list of issues is very long. I would never implement such an interface.
Consider using the canvas, via 2D or WebGL to display such content to save your self many many problems.
The first part of the answer is implemented using the canvas. The same interface view
is used in the second example that pans and zooms an element.
A simple 2D view.
As you are only panning and zooming then a very simple method can be used.
The example below implements an object called view. This holds the current scale and position (pan)
It provides two function for user interaction.
- Panning the function
view.pan(amount)
will pan the view by distance in pixels held byamount.x
,amount.y
- Zooming the function
view.scaleAt(at, amount)
will scale (zoom in out) the view byamount
(a number representing change in scale), at the position held byat.x
,at.y
in pixels.
In the example the view is applied to the canvas rendering context using view.apply()
and a set of random boxes are rendered whenever the view changes.
The panning and zooming is via mouse events
Example using canvas 2D context
Use mouse button drag to pan, wheel to zoom
const ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
const rand = (m = 255, M = m + (m = 0)) => (Math.random() * (M - m) + m) | 0;
const objects = [];
for (let i = 0; i < 100; i++) {
objects.push({x: rand(canvas.width), y: rand(canvas.height),w: rand(40),h: rand(40), col: `rgb(${rand()},${rand()},${rand()})`});
}
requestAnimationFrame(drawCanvas);
const view = (() => {
const matrix = [1, 0, 0, 1, 0, 0]; // current view transform
var m = matrix; // alias
var scale = 1; // current scale
var ctx; // reference to the 2D context
const pos = { x: 0, y: 0 }; // current position of origin
var dirty = true;
const API = {
set context(_ctx) { ctx = _ctx; dirty = true },
apply() {
if (dirty) { this.update() }
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
},
get scale() { return scale },
get position() { return pos },
isDirty() { return dirty },
update() {
dirty = false;
m[3] = m[0] = scale;
m[2] = m[1] = 0;
m[4] = pos.x;
m[5] = pos.y;
},
pan(amount) {
if (dirty) { this.update() }
pos.x += amount.x;
pos.y += amount.y;
dirty = true;
},
scaleAt(at, amount) { // at in screen coords
if (dirty) { this.update() }
scale *= amount;
pos.x = at.x - (at.x - pos.x) * amount;
pos.y = at.y - (at.y - pos.y) * amount;
dirty = true;
},
};
return API;
})();
view.context = ctx;
function drawCanvas() {
if (view.isDirty()) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
view.apply(); // set the 2D context transform to the view
for (i = 0; i < objects.length; i++) {
var obj = objects[i];
ctx.fillStyle = obj.col;
ctx.fillRect(obj.x, obj.y, obj.h, obj.h);
}
}
requestAnimationFrame(drawCanvas);
}
canvas.addEventListener("mousemove", mouseEvent, {passive: true});
canvas.addEventListener("mousedown", mouseEvent, {passive: true});
canvas.addEventListener("mouseup", mouseEvent, {passive: true});
canvas.addEventListener("mouseout", mouseEvent, {passive: true});
canvas.addEventListener("wheel", mouseWheelEvent, {passive: false});
const mouse = {x: 0, y: 0, oldX: 0, oldY: 0, button: false};
function mouseEvent(event) {
if (event.type === "mousedown") { mouse.button = true }
if (event.type === "mouseup" || event.type === "mouseout") { mouse.button = false }
mouse.oldX = mouse.x;
mouse.oldY = mouse.y;
mouse.x = event.offsetX;
mouse.y = event.offsetY
if(mouse.button) { // pan
view.pan({x: mouse.x - mouse.oldX, y: mouse.y - mouse.oldY});
}
}
function mouseWheelEvent(event) {
var x = event.offsetX;
var y = event.offsetY;
if (event.deltaY < 0) { view.scaleAt({x, y}, 1.1) }
else { view.scaleAt({x, y}, 1 / 1.1) }
event.preventDefault();
}
body {
background: gainsboro;
margin: 0;
}
canvas {
background: white;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>
Javascript zoom in/out to mouse x/y coordinates
You issue is mostly around the below lines
const x = Math.round(divMain.scrollLeft * scaleChange)
const y = Math.round(divMain.scrollTop * scaleChange)
The way scroll with scale works like below
- Calculate the unscaled
x, y
coordinate where zoom happens - Calculate the new scaled
x, y
coordinate my multiplying it with the new scale - Now you want this new coordinate remain at the same place where the existing coordinate was. So basically if you subtract the
offset x,y
from the newscaled x,y
, you get the scroll left and top.
The updated code is like below
for (const divMain of document.getElementsByClassName('main')) {
// drag the section
for (const divSection of divMain.getElementsByClassName('section')) {
// when mouse is pressed store the current mouse x,y
let previousX, previousY
divSection.addEventListener('mousedown', (event) => {
previousX = event.pageX
previousY = event.pageY
})
// when mouse is moved, scrollBy() the mouse movement x,y
divSection.addEventListener('mousemove', (event) => {
// only do this when the primary mouse button is pressed (event.buttons = 1)
if (event.buttons) {
let dragX = 0
let dragY = 0
// skip the drag when the x position was not changed
if (event.pageX - previousX !== 0) {
dragX = previousX - event.pageX
previousX = event.pageX
}
// skip the drag when the y position was not changed
if (event.pageY - previousY !== 0) {
dragY = previousY - event.pageY
previousY = event.pageY
}
// scrollBy x and y
if (dragX !== 0 || dragY !== 0) {
divMain.scrollBy(dragX, dragY)
}
}
})
}
// zoom in/out on the section
let scale = 1
const factor = 0.05
const max_scale =4
divMain.addEventListener('wheel', (e) => {
// preventDefault to stop the onselectionstart event logic
for (const divSection of divMain.getElementsByClassName('section')) {
e.preventDefault();
var delta = e.delta || e.wheelDelta;
if (delta === undefined) {
//we are on firefox
delta = e.originalEvent.detail;
}
delta = Math.max(-1,Math.min(1,delta)) // cap the delta to [-1,1] for cross browser consistency
offset = {x: divMain.scrollLeft, y: divMain.scrollTop};
image_loc = {
x: e.pageX + offset.x,
y: e.pageY + offset.y
}
zoom_point = {x:image_loc.x/scale, y: image_loc.y/scale}
// apply zoom
scale += delta*factor * scale
scale = Math.max(1,Math.min(max_scale,scale))
zoom_point_new = {x:zoom_point.x * scale, y: zoom_point.y * scale}
newScroll = {
x: zoom_point_new.x - e.pageX,
y: zoom_point_new.y - e.pageY
}
divSection.style.transform = `scale(${scale}, ${scale})`
divMain.scrollTop = newScroll.y
divMain.scrollLeft = newScroll.x
}
})
}
The updated fiddle is
https://jsfiddle.net/uy390v8t/1/
Zoom in and out on mouse click with CSS
Let's use a trick here, an input checkbox:
input[type=checkbox] {
display: none;
}
.container img {
margin: 100px;
transition: transform 0.25s ease;
cursor: zoom-in;
}
input[type=checkbox]:checked ~ label > img {
transform: scale(2);
cursor: zoom-out;
}
<div class="container">
<input type="checkbox" id="zoomCheck">
<label for="zoomCheck">
<img src="https://via.placeholder.com/200">
</label>
</div>
Related Topics
Dropdown Checkboxes and Unchecked Parents Element
Ng If with Angular for String Contains
Isotope Jquery Plugin Doesn't Show Properly on Chrome
Disable Inertia Scroll for "Single-Page" Webapp
Changing Background Color of a Specific Row in Slickgrid
How to Override Inline CSS Through JavaScript
Tinymce Customize "File" Menubar
Prevent Requestanimationframe from Running All the Time
Jquery Select a Div with a Certain Class, That Doesn't Have Another Class
Are There Any Standards for Mobile Device Web Browsers in Terms of Thread Sleeping
Provide Local Fallback for CSS from Cdn
Change Header CSS Upon Scrolling Down
How to Dynamically Change the Style Tag Using JavaScript
Bootstrap 3 Popover Arrow and Box Positioning
Animate CSS3 Gradient-Positions Using Jquery