<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dung Nguyen Thi Thuy</title>
    <description>The latest articles on DEV Community by Dung Nguyen Thi Thuy (@dung_nguyenthithuy).</description>
    <link>https://dev.to/dung_nguyenthithuy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1675842%2Fc1a1f94e-08de-4a51-a2cd-6239afb422e7.jpg</url>
      <title>DEV Community: Dung Nguyen Thi Thuy</title>
      <link>https://dev.to/dung_nguyenthithuy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dung_nguyenthithuy"/>
    <language>en</language>
    <item>
      <title>Zoom-in zoom-out a sticky point in canvas</title>
      <dc:creator>Dung Nguyen Thi Thuy</dc:creator>
      <pubDate>Mon, 24 Jun 2024 14:45:15 +0000</pubDate>
      <link>https://dev.to/dung_nguyenthithuy/zoom-in-zoom-out-a-sticky-point-in-canvas-4dnf</link>
      <guid>https://dev.to/dung_nguyenthithuy/zoom-in-zoom-out-a-sticky-point-in-canvas-4dnf</guid>
      <description>&lt;p&gt;Zoom-in zoom-out with stick point is a regular use case we meet on design or builder tools such as Figma. In this blog, I will present a basic algorithm to handle it by javascript, HTML and CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vitejsvitecsvymd-3npc--5173--9e2d28a3.local-credentialless.webcontainer.io/"&gt;Demonstration&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code step-by-step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Create a container and a scalable item&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div id="app"&amp;gt;
      &amp;lt;div class="parent"&amp;gt;
        &amp;lt;div class="scalable-child"&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.scalable-child {
  width: 300px;
  height: 300px;
  position: relative;
  top: 0;
  left: 0;
  pointer-events: none;
  transform-origin: left top;
  background-image: url('https://cdn4.vectorstock.com/i/1000x1000/17/58/caro-pattern-background-vector-2261758.jpg');
  background-size: contain;
}
.parent {
  position: relative;
  background-color: white;
  width: 100vw;
  height: 100vh;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, I use an div as a scalable item with class “scalable-child” and its container is a div with class “parent”.&lt;br&gt;
Please note for some properties: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Top, left: 0  is the default position&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pointer-event: none, because we will add event to parent, if pointer-event !== none the algorithm will be failed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transform-origin: left top, that makes coordinate origin to calculate the position&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Add wheel event listener&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const parent = document.querySelector('.parent');
const child = document.querySelector('.scalable-child');

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parent.addEventListener('wheel', wheelEventHandler, {
  passive: false,
  capture: true,
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event"&gt;WheelEvent&lt;/a&gt; to handle zoom-in, zoom-out, and moving child&lt;/p&gt;

&lt;p&gt;Note: this example demonstrates only for the trackpad. You need to handle events for hotkeys such as (Ctrl +, Ctr -) or mouse as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let left = 0;
let top = 0;
let scale = 1;

const wheelEventHandler = (e) =&amp;gt; {
  e.preventDefault();
  // Handle zoom with touch pad and hot key.
  const isZooming = e.ctrlKey || e.metaKey;
  let newValues = {};
  if (isZooming) {
    newValues = calculateOnZooming(e, scale, left, top);
  } else {
    newValues = calculateOnMoving(e, scale, left, top);
  }

  left = newValues.newLeft;
  top = newValues.newTop;
  scale = newValues.newScale;

  Object.assign(child.style, {
    transform: `scale(${scale})`,
    left: `${left}px`,
    top: `${top}px`,
  });
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Firstly, we have isZooming variable to check if zooming or moving the child element. &lt;/p&gt;

&lt;p&gt;Then we calculate the new position and scale for the child element. Left, top, and scale are being used as temperate variables.&lt;/p&gt;

&lt;p&gt;And it’s time to focus the algorithm on 2 calculate functions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Calculate on Zooming&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const calculateOnZooming = (e, oldScale, oldLeft, oldTop) =&amp;gt; {
  let newScale = oldScale - e.deltaY * oldScale * 0.01;
  newScale = Math.max(newScale, 0.1);
  const newLeft = oldLeft - (e.offsetX - oldLeft) * (newScale / scale - 1);
  const newTop = oldTop - (e.offsetY - oldTop) * (newScale / scale - 1);
  return {
    newScale,
    newLeft,
    newTop,
  };
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On zooming, wheelEvent will return the deltaY as a scale ratio and we can use it to calculate newScale&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;deltaY &amp;gt; 0 =&amp;gt; zoom-out&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;deltaY &amp;lt; 0 =&amp;gt; zoom-in&lt;br&gt;
The detalScale = e.deltaY * oldScale * 0.01 to control scaling speed&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s see the below image to more understand how to calculate the newLeft and newTop variables:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmnech48fyuz0pvjijkm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmnech48fyuz0pvjijkm.png" alt="Image description" width="800" height="516"&gt;&lt;/a&gt;&lt;br&gt;
Start zooming-in child when mouse is in A point. At that time, we can get some values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;e.offsetX: distance between mouse to parent’s left edge&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;e.offsetY: distance between mouse to parent’s top edge&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;left: current child’s left style value&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;top: current child’s top style value&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Child is scaled from scale ratio to scale’ ratio, and point A go to A’.&lt;br&gt;
So to make A point sticky (with parent), we need to calculate deltaX and deltaY then move child revert with exactly px.&lt;/p&gt;

&lt;p&gt;detalX = x’ - x&lt;br&gt;
    = x * (scale’ / scale) - x&lt;br&gt;
    = x * (scale’ / scale - 1)&lt;br&gt;
    = (e.offsetX - left) * (scale’ / scale - 1)&lt;/p&gt;

&lt;p&gt;detalY = y’ - y&lt;br&gt;
    = y * (scale’ / scale) - y&lt;br&gt;
    = y * (scale’ / scale - 1)&lt;br&gt;
    = (e.offsetY - top) * (scale’ / scale - 1)&lt;/p&gt;

&lt;p&gt;newLeft = left - detalX&lt;br&gt;
newTop = top - detalY&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Calculate on Moving&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const calculateOnMoving = (e, oldScale, oldLeft, oldTop) =&amp;gt; {
  return {
    newLeft: oldLeft - e.deltaX * 2,
    newTop: oldTop - e.deltaY * 2,
    newScale: oldScale,
  };
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On moving event, we need to calculate only newLeft and newTop values. And we *2  each delta value to increase speed as well.&lt;/p&gt;

&lt;p&gt;That’s all we need to handle. I hope it’s helpful. Thank you for watching!&lt;br&gt;
You can view the full source code &lt;a href="https://stackblitz.com/edit/vitejs-vite-csvymd?file=index.html"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>css</category>
      <category>html</category>
      <category>canvas</category>
    </item>
  </channel>
</rss>
