Button to toggle Zoom (Pinch-zoom in/out)

In what follows is an attempt to have a button that toggles zoom in and out at the back of my card. At the back field of my card is a scroll captured image that mimics the view of a pdf in addition to some text.

I had hoped that when the button is clicked on, the middle of my current screen (which has the said image) will be zoomed in and out on. Instead the zoom happens on at a different part of the image. Also, when the zoom/scale(2) happens, I can scroll to all parts of the right side of the image but not the left.

The most important thing is that the zoom has to happen in the middle of the screen pretty much like you would a normal android pinch-to-zoom on a pdf. Any assistance is welcomed. Thanks already.

Back Template:

<button class="setzoom" onclick="toggleZoom();" id="myBtn3b"></button>
<button class="spoiler" id="Back" style="display:; padding:0; font-size: 16px; text-align: left;" onclick="{document.getElementById('Back') .style.display='none'}"><div id="contentField">{{Back_m}}</div></button>
<script>
// Get the button
let zoomedIn = false;

function toggleZoom() {
    const firstImage = document.querySelector("#contentField img:first-of-type");
    const button = document.getElementById("myBtn3b");

    if (firstImage) {
        if (zoomedIn) {
            firstImage.style.transform = "scale(1)";
        } else {
            firstImage.style.transform = "scale(2)";
        }

        zoomedIn = !zoomedIn;
    } else {
      document.getElementById("myBtn3b").innerHTML="!Img";
    }
}
</script>

CSS

img {
  display: block;
  margin-left: auto;
  margin-right: auto;
  max-width: auto;
  width:auto;
  max-height:100%;
}

#myBtn3b {
  position: fixed;
  bottom: 175px;
  right: 33px;
  z-index: 99;
  border: none;
  outline: none;
  background-color: rgba(133, 133, 133,0.5);
  padding: 26px;
  padding-top: 24px;
  padding-bottom: px;
  border-radius: 5px;
}

#contentField {
    position: relative;
    overflow: auto;
    max-width: 100%; 
}

#contentField img:first-of-type {
    max-width: 100%;
    transition: transform 0.3s;
    transform-origin: center;
}
1 Like

I don’t think I got what you wanted from your description but how about this. Instead of scaling the image and showing scrollbars, allow the image to overflow so it fills your screen when zoomed. This feels better to me compared to having the image constrained within the viewport. Currently this will push all other elements around as it resizes though.

Template

<button class="setzoom" onclick="toggleZoom();" id="myBtn3b"></button>
<button
  class="spoiler"
  id="Back"
  style="display: ; padding: 0; font-size: 16px; text-align: left"
  onclick="{document.getElementById('Back') .style.display='none'}"
>
  <div id="contentField">{{Back}}</div>
</button>
<script>
  var zoomedIn = false;

  function toggleZoom() {
    const firstImage = document.querySelector(
      "#contentField img:first-of-type"
    );
    const zoomButton = document.getElementById("myBtn3b");

    if (firstImage) {
      // The image is allowed to overflow its container so we can control its "zoom"
      // by changing the width of the image, it will then increase in size and overflow
      // the container and scrolling around the image will be possible
      if (zoomedIn) {
        firstImage.style.width = "100%";
      } else {
        firstImage.style.width = "200%";
      }

      zoomedIn = !zoomedIn;
    } else {
      zoomButton.innerHTML = "!Img";
    }
  }
</script>

Styling

img {
  display: block;
  margin-left: auto;
  margin-right: auto;
  /* override the max-width: 100% and max-height: 95vh set in the default reviewer.css */
  max-width: none;
  max-height: none;
  /* allow the image to overflow the viewport */
  overflow: visible;
  /* however initially we want to the image to fit the viewport
     zooming in and out will be controlled by changing the width */
  width: 100%;
}

#myBtn3b {
  position: fixed;
  bottom: 175px;
  right: 33px;
  z-index: 9999;
  border: none;
  outline: none;
  background-color: rgba(133, 133, 133,0.5);
  padding: 26px;
  padding-top: 24px;
  padding-bottom: px;
  border-radius: 5px;
}

#contentField {
    position: relative;
    max-width: 100%; 
}

#contentField img:first-of-type {
    transition: width 0.3s;
    transform-origin: center;
}

1 Like

Scaling doesn’t happen at all.

Unless I change firstImage.style.width="200%"; to first Image.style.transform="scale(2)"; and in the css, transition: width 0.3s; to transition: transform 0.3s;

This is what GPT is also saying (tried but isn’t working):

The issue seems to arise from the transform-origin not being set to the center of the visible area when the zoom happens, and the overflow styling not allowing proper scrolling to the left. Here’s how you can fix it:

Key Fixes:

  1. Ensure Transform Origin is Correctly Set: Modify the transform-origin dynamically to focus on the center of the visible area when zooming.
  2. Fix Scroll Overflow: Ensure the container allows full scrolling in all directions when zoomed in.
  3. Enhance Responsiveness: Add dynamic centering logic so the zoom focuses on the middle of the visible screen rather than the default transform origin.

Here’s your updated code:

Back Template

<button class="setzoom" onclick="toggleZoom();" id="myBtn3b"></button>
<button class="spoiler" id="Back" style="display:; padding:0; font-size: 16px; text-align: left;" onclick="{document.getElementById('Back').style.display='none'}">
  <div id="contentField">{{Back_m}}</div>
</button>
<script>
let zoomedIn = false;

function toggleZoom() {
    const firstImage = document.querySelector("#contentField img:first-of-type");
    const contentField = document.getElementById("contentField");

    if (firstImage) {
        if (zoomedIn) {
            // Zoom out
            firstImage.style.transform = "scale(1)";
            firstImage.style.transformOrigin = "center center";
            contentField.scrollTo(contentField.scrollWidth / 2, contentField.scrollHeight / 2);
        } else {
            // Calculate the center of the visible area
            const rect = contentField.getBoundingClientRect();
            const centerX = rect.width / 2 + contentField.scrollLeft;
            const centerY = rect.height / 2 + contentField.scrollTop;

            // Set transform-origin dynamically to the center of the visible area
            firstImage.style.transformOrigin = `${centerX}px ${centerY}px`;
            firstImage.style.transform = "scale(2)";
        }

        zoomedIn = !zoomedIn;
    } else {
        document.getElementById("myBtn3b").innerHTML = "!Img";
    }
}
</script>

CSS

img {
  display: block;
  margin-left: auto;
  margin-right: auto;
  max-width: auto;
  width: auto;
  max-height: 100%;
}

#myBtn3b {
  position: fixed;
  bottom: 175px;
  right: 33px;
  z-index: 99;
  border: none;
  outline: none;
  background-color: rgba(133, 133, 133, 0.5);
  padding: 26px;
  padding-top: 24px;
  border-radius: 5px;
}

#contentField {
  position: relative;
  overflow: auto;
  max-width: 100%;
  height: 500px; /* Adjust based on your card's layout */
}

#contentField img:first-of-type {
  max-width: 100%;
  transition: transform 0.3s ease-in-out;
}

I gather ankidriod’s framework makes it more or less a browser of a sort. As such, if I could zoom in on chrome whilst I browse, I wonder why it seem so difficult to achieve same on ankidriod.

Attached are links to sample.apkg and a demonstration video. In the video, a Tasker scene is used to produce the exact toggle effect am hoping to achieve with an internal Ankidroid button (myBtn2b).

Can someone please :pray: help with a working function for “togglezoom()”. Thank you in advance

1 Like

For those who tried and could not access shared files, access permission has been revised :pray:

This seems to work about like your video.

One thing I couldn’t fix is that the zoom button’s fixed positioning is broken by zooming. Apparently, in AnkiDroid increasing the document’s size causes the viewport to become bigger such that the actual screen no longer is showing the viewport? This is really weird and of course breaks things like position: fixed.

I added some console.logs that let you see this for yourself. Uncomment those and the eruda import at the top to see the console in AnkiDroid.

Changing the fixed positioning of the s12 div to sticky instead kind of helps but again gets messed up when the image is zoomed. Somehow, the bottom of the viewport still stays the same with position: sticky where it doesn’t with position: sticky. I don’t get what the hell is going on in AnkiDroid’s reviewer webview.

Back

<!-- <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>
  eruda.init();
</script> -->
<div id="answer">{{Front}}</div>
<hr />
<button onclick="document.documentElement.scrollTop = fieldContent;" id="myBtn">
  ¤
</button>
<button class="setzoom" onclick="toggleZoom()" id="myBtn2b"></button>
<script>
  var zoomedIn = false;

  function toggleZoom() {
    const firstImage = document.querySelector("#Back img:first-of-type");
    const contentField = document.getElementById("Back");
    const zoomButton = document.getElementById("myBtn2b");
    // console.log(
    //   "viewport width/height before zoom",
    //   window.innerWidth,
    //   window.innerHeight
    // );

    if (firstImage) {
      if (zoomedIn) {
        // Zoom out
        firstImage.style.transform = "scale(1)";
        firstImage.style.transformOrigin = "center center";
        firstImage.style.left = "0px";
        firstImage.style.top = "0px";
        contentField.scrollTo(
          contentField.scrollWidth / 2,
          contentField.scrollHeight / 2
        );
      } else {
        // Calculate the center of the visible area
        const rect = contentField.getBoundingClientRect();
        const centerX = rect.width / 2 + contentField.scrollLeft;
        const centerY = rect.height / 2 + contentField.scrollTop;

        // Set transform-origin dynamically to the center of the visible area
        firstImage.style.transformOrigin = `${centerX}px ${centerY}px`;
        firstImage.style.transform = "scale(2)";
        // Get image width and set left: to half of it
        firstImage.style.left = firstImage.width / 2 + "px";
        // Set top: to half of the image height
        firstImage.style.top = firstImage.height / 2 + "px";

        // console.log(
        //   "viewport width/height after zoom",
        //   window.innerWidth,
        //   window.innerHeight
        // );
      }

      zoomedIn = !zoomedIn;
    } else {
      document.getElementById("myBtn2b").innerHTML = "!Img";
    }
  }
</script>
<button
  class="spoiler"
  id="Back"
  style="display: ; padding: 0; font-size: 16px; text-align: left"
  onclick="{document.getElementById('Back') .style.display='none'}"
>
  {{Back}}</button
><br />
<hr />
<div id="sl2">{{Answer}}</div>
<script>
  var fieldContentDK = "{{Jump}}";
  var isAnkiDroid = /wv/i.test(navigator.userAgent);
</script>
<script>
  // Get the button
  var fieldContent = "{{Jump}}";
  var locs = "";
  var cnt = 0;

  document.onkeyup = function (event) {
    if (event.code == "BracketRight") {
      cnt += 1;
      locs += cnt + "¤ " + window.scrollY + ", ";
      alert(locs);
    }
  };

  if (isAnkiDroid) {
    setTimeout(function () {
      document.documentElement.scrollTop = fieldContent;
    }, 200);
  } else {
    setTimeout(function () {
      document.documentElement.scrollTop = fieldContentDK;
    }, 0);
  }
</script>

Styling

@import url("_editor_button_styles.css");

.card {
  font-family: arial;
  font-size: 20px;
  text-align: center;
  color: black;
  background-color: white;
}

img {
  display: block;
  margin-left: auto;
  margin-right: auto;
  max-width: auto;
  width: auto;
  max-height: 100%;
  position: relative;
}

hr {
  background: blue;
  height: 1.5px;
  border: 0;
  margin: 0.1em;
}

#myBtn {
  position: fixed;
  bottom: 65px;
  right: 30px;
  z-index: 99;
  font-size: 37px;
  border: none;
  outline: none;
  background-color: rgba(255, 0, 0, 0.5);
  color: white;
  padding: 15px;
  padding-top: 2px;
  padding-bottom: 5px;
  border-radius: 4px;
}

#myBtn2b {
  position: fixed;
  bottom: 120px;
  right: 33px;
  z-index: 99;
  border: none;
  outline: none;
  background-color: rgba(133, 133, 133, 0.5);
  padding: 26px;
  padding-top: 24px;
  padding-bottom: px;
  border-radius: 5px;
}

#answer {
  position: sticky;
  top: 0px;
  background-color: ghostwhite;
  color: black;
  z-index: 99;
  border: 1px solid lightgrey;
  width: 105%;
  font-size: 12px;
  left: 0px;
  max-height: 60px; /* Change the value to your desired height */
  overflow: auto; /* Enable scrolling if content exceeds height limit */
}

#sl2 {
  position: sticky;
  bottom: 0px;
  background-color: ghostwhite;
  color: black;
  z-index: 99;
  border: 1px solid lightgrey;
  width: 100%;
  font-size: 12px;
  left: 0px;
  max-height: 60px; /* Change the value to your desired height */
  overflow: auto; /* Enable scrolling if content exceeds height limit */
}

1 Like

Thanks for the response @jhhr. As you put it though, it does “seem” to work. I have been on this close to two months and I still haven’t been able to get it quiet right… the most disturbing of them all being the focus area of the zoom;

  • The zoom action (as demonstrated in the video) is expected to happen right in the middle of the current view (ie the very portion of the scroll-captured image that’s currently showing or otherwise).

// Calculate the center of the visible area

Apparently, on my 5.1-inch phone, the “Calculate the center…” part isn’t giving the required output.

Yeah, that’s going to be hard nut to crack due to the weird way that AnkiDroid changes viewport size on the zoom. You could try to

  1. take the centerX and centerY coordinates,
  2. figure out the position of those coordinates relatively to the image size before zoom: something like const relX = centerX / image.width and relY = centerY / image.height (maybe it’d be better to simply use contentField.scrollLeft as the transformed coordinate?)
  3. and then after zoom, change the scroll position with window.scrollTo onto zoom-adjusted coordinates: something like window.scrollTo(image.width * relX, image.height * relY)

My example calculation of multiplying by a simple ratio is likely wrong but some transformation of the coordinates should result in your intended effect.

1 Like

Thank you

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.