1

I am trying to build an application where you can zoom a bit with the mouse wheel into one point, and then later zoom further in at another point.

In other words, the point of "origin" for the zooming can change along the way.

Take a look at this example: https://codesandbox.io/s/4w4m1k5zlx

var phase = 1;

var box1 = document.getElementById("box1");
var box2 = document.getElementById("box2");

box1.style.transformOrigin = "0 0";
box2.style.transformOrigin = "0 0";

var width = 100;
var height = 100;

function transform(originX, originY, translateX, translateY, scale) {
  transformElement(1, box1, 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";

  if (method === 1) {
    element.style.transform = `translate(${originX}px, ${originY}px) translate(${translateX}px, ${translateY}px) scale(${scale}) translate(-${originX}px, -${originY}px)`;
  } else if (method === 2) {
    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(box1);
  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: 60px;
  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="box1" class="box"></div>
</div>
<div class="container">
  <div class="box outline">
  </div>
  <div id="box2" class="box"></div>
</div>

Top box in the example: Here I tried simulating transform-origin with transform in order to be able to transition one property. However, the animation is not "even" - especially during the first scale (where it will zoom a bit in and out again - hard to explain but I hope you can see it).

Bottom box in the example: Changing both the transform-origin and transform at the same time, it becomes quite jumpy, because transform has a transition and transform-origin doesn't.

The top example is most ideal, but it still doesn't look good because of the unsmooth zooming. How can I zoom on points at different phases without making the translation either jump or zoom in and out again?

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
Mathias Lykkegaard Lorenzen
  • 15,031
  • 23
  • 100
  • 187

1 Answers1

3

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>

UPDATE

Considering the option 1, the zoom-in/out effect is (I think) due to the fact that you are using two translations (to simulate transform-origin) that are surrounding the main transformation you want to do. So the effect is something like : Move to the point A, scale the element, move to point B.

Here is a simple example to illustrate the issue of this bouncing effect. We can clearly see a slight movement to the right then to the left.

.box {
  width:100px;
  height:100px;
  margin-left:200px;
  background:red;
  transition:2s all;
  transform-origin:0 0;
}

body:hover .box{
  transform:translate(100px,0) scale(2) translate(-100px,0);
}

body {
 margin:0;
 background:repeating-linear-gradient(to right,transparent 0px,transparent 98px,#000 98px,#000 100px)
}
<div class="box">

</div>

Our goal is to avoid that and make the element go straight to its final position while doing the scale. An idea is to split the transform in 2 parts. The trick is to apply one translation with the scale then later apply the other translation.

Here is an example:

document.querySelector('body').addEventListener('mouseover',function(){
  document.querySelector('.box').style.transform="scale(2) translate(-100px,0)";
  setTimeout(function(){
      document.querySelector('.box').style.transform="translate(100px,0) scale(2) translate(-100px,0)";
  },500);
})
document.querySelector('body').addEventListener('mouseleave',function(){
  document.querySelector('.box').style.transform="none";
})
.box {
  width:100px;
  height:100px;
  margin-left:200px;
  background:red;
  transition:1s all linear .5s;
  transform-origin:0 0;
}

body {
 margin:0;
 background:repeating-linear-gradient(to right,transparent 0px,transparent 98px,#000 98px,#000 100px)
}
<div class="box">

</div>

As we can see we no more have the bouncing effect. The red div is no more going to the right then to the left but only going to the left. The idea is somehow crazy and difficult to explain but the trick is the use of delay.

On hover, we add the transform with one translation and scale then due to the delay the div won't move. Right after the end of the delay, we change the transform by adding the other translation. This will make the div to only scale considering its new origin that we simulated with both translations.


Here is the full code:

var phase = 1;

var box1 = document.getElementById("box1");
var box2 = document.getElementById("box2");


var width = 100;
var height = 100;

function transform(originX, originY,  scale) {
  transformElement(1, box1, originX, originY, scale);
  transformElement(2, box2, originX, originY, scale);
}

function transformElement(
  method,
  element,
  originX,
  originY,
  scale
) {

  if (method === 1) {
    element.style.transform =`scale(${scale}) translate(-${originX}px, -${originY}px)`;
    setTimeout(function(){
    element.style.transform = `translate(${originX}px, ${originY}px) scale(${scale}) translate(-${originX}px, -${originY}px)`;
    },200,element,scale,originX,originY)
    
    
  } else if (method === 2) {
    element.style.transformOrigin = `${originX}px ${originY}px`;
    element.style.transform = ` 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(box1);
  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, 1.5);
}

function phase2() {
  transform(width * 0.25, height / 2,  2);
}

function phase3() {
  transform(width / 2, height,  2.5);
}

function phase4() {
  transform(width / 2, 0,  3);
}

const phases = [reset, phase1, phase2, phase3, phase4];

setInterval(() => phases[phase++ % phases.length](), 1400);
* {
  box-sizing: border-box;
}

.container {
  position: relative;
  display:inline-block;
  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;
  transition:transform 1s linear 0.2s, transform-origin 0.2s;
}

.outline {
  background-color: transparent;
  border: 1px solid black;
}
<div class="container">
  <div class="box outline">
  </div>
  <div id="box1" class="box"></div>
</div>
<div class="container">
  <div class="box outline">
  </div>
  <div id="box2" class="box"></div>
</div>

I simplified the JS and I kept the first attempt that rely on changing the transform-origin first in order to compare.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • The master has spoken! Unfortunately, changing the origin first has not been considered acceptable for the product I am developing. – Mathias Lykkegaard Lorenzen Jul 11 '18 at 10:04
  • 1
    @MathiasLykkegaardLorenzen in this case you can keep the idea and try to play with delay and timing function. you can for example change them at the same time but make the transform to have an ease-in effect and origin and eas-out .. something like this : https://jsfiddle.net/2h4oaeu1/1/ – Temani Afif Jul 11 '18 at 10:09
  • Thanks, for unfortunately when pinch-zooming (which is also supported in my real application), this will look funny when changing origins after a partly zoomed in content. I really believe we have to make option 1 work somehow. What do you think? – Mathias Lykkegaard Lorenzen Jul 11 '18 at 10:43
  • 1
    @MathiasLykkegaardLorenzen option 1 is not easy to handle due to the complexity of how we conbine translation and scale. I tried to play with it but it wasn't easy :/ since all the properties are within one transform – Temani Afif Jul 11 '18 at 10:50
  • 1
    @MathiasLykkegaardLorenzen by the way let's wait, more masters will come with other ideas ;) – Temani Afif Jul 11 '18 at 11:11
  • @MathiasLykkegaardLorenzen Yes saw it ;) well, I will try to deep more on it :p – Temani Afif Jul 16 '18 at 08:21
  • @MathiasLykkegaardLorenzen check the update, I think I got something ;) – Temani Afif Jul 16 '18 at 14:14
  • Very nice, and very close! But is it possible to elliminate the delay that happens between zooms in the final example to the left? – Mathias Lykkegaard Lorenzen Jul 16 '18 at 14:18
  • @MathiasLykkegaardLorenzen the main trick is the use of delay :p ... well if you got the trick I guess you can try to reduce it to make it non visible .. maybe you can try something like 0.2s ? – Temani Afif Jul 16 '18 at 14:20
  • @MathiasLykkegaardLorenzen Ah you need to also change the setInterval, I forget it .. check now. you need to make it equal to the total time needed for the transition (1s + 2*delay = 1.4s) – Temani Afif Jul 16 '18 at 14:22