Zooming on a Point with CSS3 Transform Scale

Zooming on a point with CSS3 transform scale

One thing to watch out for when using transforms is the order that you apply them. You'll find your example works rather differently if you switch the scale and the translate around.

Here is an interesting article on the matter:

https://staff.washington.edu/fmf/2011/07/15/css3-transform-attribute-order/

I wasn't able to repair your version, mainly because it misbehaves unexpectedly when you switch the order of the transforms. Basically it seems you are running into odd behaviour because the scale itself causes an automatic translation in position, and then you also translate... and it seems these different translations are occurring at a slightly different pace.

I did however re-implement a version that works, and allows you to translate before scaling. Keeping the transforms in this order seems to avoid the issue.

http://jsfiddle.net/fxpc5rao/32/

I've modified the version below to use translate3D just because it performs better for many systems.

var current = {x: 0, y: 0, zoom: 1},    con = document.getElementById('container');    window.onclick = function(e) {    var coef = e.shiftKey || e.ctrlKey ? 0.5 : 2,        oz = current.zoom,        nz = current.zoom * coef,        /// offset of container        ox = 20,        oy = 20,        /// mouse cords        mx = e.clientX - ox,        my = e.clientY - oy,        /// calculate click at current zoom        ix = (mx - current.x) / oz,        iy = (my - current.y) / oz,        /// calculate click at new zoom        nx = ix * nz,        ny = iy * nz,        /// move to the difference        /// make sure we take mouse pointer offset into account!        cx = mx - nx,        cy = my - ny    ;    // update current    current.zoom = nz;    current.x = cx;    current.y = cy;    /// make sure we translate before scale!    con.style.transform        = 'translate3D('+cx+'px, '+cy+'px,0) '        + 'scale('+nz+')'    ;};
#container {    position: absolute;    left: 20px;    top: 20px;    width: 100%;    height: 100%;    transform-origin: 0 0 0;    transition: transform 0.3s;    transition-timing-function: ease-in-out;    transform: translate3D(0,0,0) scale(1);}
#item { position: absolute;}
<div id="container">    <div id="item">        <img src="http://fadili.users.greyc.fr/demos/WaveRestore/EMInpaint/parrot_original.png" />    </div></div>

Using CSS transform scale() to zoom into an element without cropping, maintaining scrolling

Why not just to reposition the TransformOrigin to 0 0 and to use proper scrollTop/scrollLeft after the animation?

  • https://jsfiddle.net/b8vLg0ny/7/
  • Updated: https://jsfiddle.net/b8vLg0ny/13/

If you do not need the animation, the TransformOrigin can always stays 0 0 and only the scrolling is used to show the box.

To make the animation less jumpy use transition only for transform porperty, otherwise the transform-origin gets animated also. I have edited the example with 4x4 elements, but I think it makes sense to zoom a box completely into view, thats why I changed the zoom level. But if you stay by zoom level 2 and the grid size 15x15 for instance, then with this approach really precise origin should be calculated for transform, and then also the correct scrolling.

Anyway I don't know, if you find this approach useful.

Stack snippet

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>

CSS scaling from a specific point when that point may change along the way?

I think the best you can do is to rely on the option 2. You can apply a transition to the transform-origin AND add a delay to transform so that you first change the origin then you do the transform:

transform 1s linear 0.5s, transform-origin 0.5s

Full code:

var phase = 1;
var box2 = document.getElementById("box2");
box2.style.transformOrigin = "0 0";
var width = 100;var height = 100;
function transform(originX, originY, translateX, translateY, scale) { transformElement(2, box2, originX, originY, translateX, translateY, scale);}
function transformElement( method, element, originX, originY, translateX, translateY, scale) { element.style.transition = "transform 1s linear 0.5s,transform-origin 0.5s";
element.style.transformOrigin = `${originX}px ${originY}px`; element.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
var pointElement = document.createElement("div"); pointElement.classList.add("point"); pointElement.style.transform = `translate(${originX}px, ${originY - 2 * scale}px)`;
element.appendChild(pointElement);}
function reset() { resetElement(box2);}
function resetElement(element) { while (element.children.length > 0) { element.removeChild(element.children[0]); }
element.style.transform = ""; element.style.transition = "";
void element.clientWidth;}
function phase1() { transform(width * 0.75, height / 2, 0, 0, 1.5);}
function phase2() { transform(width * 0.25, height / 2, 0, 0, 2);}
function phase3() { transform(width / 2, height, 0, 0, 2.5);}
function phase4() { transform(width / 2, 0, 0, 0, 3);}
const phases = [reset, phase1, phase2, phase3, phase4];
setInterval(() => phases[phase++ % phases.length](), 1500);
* {  box-sizing: border-box;}
body { background-color: black;}
.container { position: relative; margin: 50px; background-color: lightgray; width: 200px; height: 200px;}
.point { width: 2px; height: 2px; background-color: white;}
.box { position: absolute; top: 25%; left: 25%; transform-origin: 0 0; background-color: teal; opacity: 0.8; width: 100px; height: 100px;}
.outline { background-color: transparent; border: 1px solid black;}
<div class="container">  <div class="box outline">  </div>  <div id="box2" class="box"></div></div>

Zoom in on a point (using scale and translate)

Finally solved it:

const zoomIntensity = 0.2;

const canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
const width = 600;
const height = 200;

let scale = 1;
let originx = 0;
let originy = 0;
let visibleWidth = width;
let visibleHeight = height;

function draw(){
// Clear screen to white.
context.fillStyle = "white";
context.fillRect(originx, originy, width/scale, height/scale);
// Draw the black square.
context.fillStyle = "black";
context.fillRect(50, 50, 100, 100);

// Schedule the redraw for the next display refresh.
window.requestAnimationFrame(draw);
}
// Begin the animation loop.
draw();

canvas.onwheel = function (event){
event.preventDefault();
// Get mouse offset.
const mousex = event.clientX - canvas.offsetLeft;
const mousey = event.clientY - canvas.offsetTop;
// Normalize mouse wheel movement to +1 or -1 to avoid unusual jumps.
const wheel = event.deltaY < 0 ? 1 : -1;

// Compute zoom factor.
const zoom = Math.exp(wheel * zoomIntensity);

// Translate so the visible origin is at the context's origin.
context.translate(originx, originy);

// Compute the new visible origin. Originally the mouse is at a
// distance mouse/scale from the corner, we want the point under
// the mouse to remain in the same place after the zoom, but this
// is at mouse/new_scale away from the corner. Therefore we need to
// shift the origin (coordinates of the corner) to account for this.
originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;

// Scale it (centered around the origin due to the trasnslate above).
context.scale(zoom, zoom);
// Offset the visible origin to it's proper position.
context.translate(-originx, -originy);

// Update scale and others.
scale *= zoom;
visibleWidth = width / scale;
visibleHeight = height / scale;
}
<canvas id="canvas" width="600" height="200"></canvas>

Scale/zoom a DOM element and the space it occupies using CSS3 transform scale()

The HTML (Thanks Rory)

<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Sandbox for Stack Overflow question http://stackoverflow.com/q/10627306/578288" />
<meta charset=utf-8 />
<title>Sandbox for SO question about scaling an element both visually and dimensionally</title>
</head>
<body>

<div id="wrapper">
<div class="surrounding-content">
before
</div>

<div id="content-to-scale">
<div>something inside</div>
<div><img src="http://placekitten.com/g/150/100"></div>
<div>another something</div>
</div>

<div class="surrounding-content">
after
</div>
</div>

</body>
</html>

The CSS (Still started from Rory's base)

body {
font-size: 13px;
background-color: #fff;
}
#wrapper {
width: 50%;
margin-left: auto;
margin-right: auto;
border: 0.07692307692307693em solid #888;
padding: 1.1538461538461537em;
}
.surrounding-content {
border: 0.07692307692307693em solid #eee;
}
#content-to-scale {
border: 0.07692307692307693em solid #bbb;
width: 10em;
}
#content-to-scale {
font-size: 1.1em;
}
#content-to-scale img {
width: auto;
height: auto;
min-width: 100%;
max-width: 100%;
}

The Explanation:

I'm using font size and ems to "scale" the dimensions of the child elements.

Ems are dimension units that are relative to the current context's font-size.

So if I say I have a font-size of 13px and a border of 1 (the desired border-width in pixels) divded
by 13 (the current context's font-size also in pixels) = 0.07692307692307693em the browser ought to render a 1px border

To emulate a 15px padding I use the same formula, (desired pixels)/(current context's font-size in pixels) = desired ems.
15 / 13 = 1.1538461538461537em

To tame the scaling of the image I use an old favorite of mine: the natural ratio preserving scale, let me explain:

Images have a natural height and width and a ratio between them. Most browser's will preserve this ratio if both width and height are set to auto.
You can then control the desired width with min-width and max-width, in this case making it always scale to the full width of the parent element, even when it will scale beyond it's natural width.

(You can also use max-width and max-height 100% to prevent the image from busting out of the borders of the parent element, but never scaling beyond their natural dimensions)

You can now control the scaling by tweaking the font-size on #content-to-scale. 1.1em roughly equals scale(1.1)

This does have some drawbacks: nested font-sizing in ems are applied recusively. Meaning if you have:

<style type="text/css">
div{
font-size: 16px;
}
span{
font-size: 0.5em;
}
</style>
<div>
<span>
<span>
Text
</span>
</span>
</div>

You will end up with "Text" rendering at 4px instead of the 8px you might expect.



Related Topics



Leave a reply



Submit