<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>JavaScript on David Hamann</title><link>https://davidhamann.de/tags/javascript/</link><description>Recent content in JavaScript on David Hamann</description><generator>Hugo</generator><language>en</language><copyright>&amp;copy; David Hamann</copyright><lastBuildDate>Fri, 13 Jan 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://davidhamann.de/tags/javascript/feed.xml" rel="self" type="application/rss+xml"/><item><title>SVG and JavaScript: transform viewport coordinates into element coordinates</title><link>https://davidhamann.de/2023/01/13/svg-javascript-transform-viewport-to-element-coordinates/</link><pubDate>Fri, 13 Jan 2023 00:00:00 +0000</pubDate><guid>https://davidhamann.de/2023/01/13/svg-javascript-transform-viewport-to-element-coordinates/</guid><description>&lt;p&gt;A couple of months ago I built a JavaScript application that allows adding points and labels to locations on a building floorplan. The whole canvas (not HTML &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;) is a SVG document inside an HTML document and points/objects/labels/etc. are added to that canvas as native SVG elements.&lt;/p&gt;
&lt;p&gt;Users can add/move objects on the floorplan but also zoom and pan the floorplan itself.&lt;/p&gt;
&lt;p&gt;When performing these actions it is important to transform coordinates from the screen or viewport (like the position of your mouse/fingers) into coordinates that make sense in your SVG element&amp;rsquo;s coordinate system.&lt;/p&gt;
&lt;p&gt;In this post I want to share some of my notes and a simplified example on how to achieve this using a &lt;code&gt;group&lt;/code&gt; element with a &lt;code&gt;transform&lt;/code&gt; attribute and a few objects (&lt;code&gt;circle&lt;/code&gt; elements) inside.&lt;/p&gt;
&lt;h2 id="panning-zooming-dragging"&gt;Panning, Zooming, Dragging&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s say we have a setup as shown in the following Gif:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Zooming, panning and dragging" loading="lazy" src="https://davidhamann.de/images/svg-transform.gif"&gt;&lt;/p&gt;
&lt;p&gt;We can move the canvas, we can move objects, zoom in and then move objects again in a zoomed/panned state.&lt;/p&gt;
&lt;p&gt;If you look at the inspector window you see that the zooming and panning is performed by changing the &lt;code&gt;transform&lt;/code&gt; attribute on the group element with id &lt;code&gt;main&lt;/code&gt;, and that moving an object (a &lt;code&gt;circle&lt;/code&gt; element) just changes the respective &lt;code&gt;cx&lt;/code&gt; and &lt;code&gt;cy&lt;/code&gt; values (as the objects are inside the transformed group).&lt;/p&gt;
&lt;p&gt;All the movements follow the mouse pointer in a reasonably fast manner.&lt;/p&gt;
&lt;h2 id="implementing-the-start-of-the-pan"&gt;Implementing the start of the pan&lt;/h2&gt;
&lt;p&gt;A first (incomplete) approach to implementing the panning or dragging could be to store &lt;code&gt;clientX&lt;/code&gt; and &lt;code&gt;clientY&lt;/code&gt; from the &lt;code&gt;mousedown&lt;/code&gt; or &lt;code&gt;touchstart&lt;/code&gt; event and then calculate the difference to the &lt;code&gt;clientX&lt;/code&gt; and &lt;code&gt;clientY&lt;/code&gt; from the &lt;code&gt;mousemove&lt;/code&gt; or &lt;code&gt;touchmove&lt;/code&gt; event. While this difference represents the amount the mouse moved in the browser&amp;rsquo;s viewport (window), it cannot be directly applied to the transformation matrix of our &lt;code&gt;main&lt;/code&gt; element as the SVG document on the page may not cover the whole viewport, might be in a scaled state, etc. – in any case, we have at least two different coordinate systems.&lt;/p&gt;
&lt;p&gt;The effect when not translating the coordinates is usually that objects move either much faster or much slower than your mouse pointer.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s now see how we can transform the coordinates you get from events to coordinates you can use in your SVG document.&lt;/p&gt;
&lt;p&gt;A minimal HTML document with an SVG document inside (as seen in the Gif above) could look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;html&amp;gt;
 &amp;lt;head&amp;gt;
 &amp;lt;title&amp;gt;SVG app&amp;lt;/title&amp;gt;
 &amp;lt;meta charset=&amp;#34;utf-8&amp;#34; /&amp;gt;
 &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1&amp;#34;&amp;gt;
 &amp;lt;/head&amp;gt;
 &amp;lt;body&amp;gt;
 &amp;lt;div id=&amp;#34;canvas-wrapper&amp;#34;&amp;gt;
 &amp;lt;svg
 id=&amp;#34;canvas&amp;#34;
 viewBox=&amp;#34;0 0 1024 768&amp;#34;
 preserveAspectRatio=&amp;#34;xMidYMid&amp;#34;
 xmlns=&amp;#34;http://www.w3.org/2000/svg&amp;#34;
 &amp;gt;
 &amp;lt;g
 id=&amp;#34;main&amp;#34;
 transform=&amp;#34;matrix(1 0 0 1 0 0)&amp;#34;
 &amp;gt;
 &amp;lt;image
 x=&amp;#34;0&amp;#34;
 y=&amp;#34;0&amp;#34;
 width=&amp;#34;1024&amp;#34;
 height=&amp;#34;768&amp;#34;
 xlink:href=&amp;#34;background.png&amp;#34;
 &amp;gt;&amp;lt;/image&amp;gt;
 &amp;lt;!-- some &amp;#34;random&amp;#34; points --&amp;gt;
 &amp;lt;circle id=&amp;#34;p1&amp;#34; cx=&amp;#34;512&amp;#34; cy=&amp;#34;284&amp;#34; r=&amp;#34;4&amp;#34; fill=&amp;#34;blue&amp;#34; stroke=&amp;#34;black&amp;#34; /&amp;gt;
 &amp;lt;circle id=&amp;#34;p2&amp;#34; cx=&amp;#34;500&amp;#34; cy=&amp;#34;300&amp;#34; r=&amp;#34;4&amp;#34; fill=&amp;#34;red&amp;#34; stroke=&amp;#34;black&amp;#34;/&amp;gt;
 &amp;lt;circle id=&amp;#34;p3&amp;#34; cx=&amp;#34;490&amp;#34; cy=&amp;#34;330&amp;#34; r=&amp;#34;4&amp;#34; fill=&amp;#34;red&amp;#34; stroke=&amp;#34;black&amp;#34; /&amp;gt;
 &amp;lt;circle id=&amp;#34;p4&amp;#34; cx=&amp;#34;430&amp;#34; cy=&amp;#34;250&amp;#34; r=&amp;#34;4&amp;#34; fill=&amp;#34;red&amp;#34; stroke=&amp;#34;black&amp;#34; /&amp;gt;
 &amp;lt;/g&amp;gt;
 &amp;lt;/svg&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let&amp;rsquo;s say we want to start our panning action when the user clicks on the div with the id &lt;code&gt;canvas-wrapper&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Once the document is ready, we register the event listener, for example like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;canvas-wrapper&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mousedown&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPanStart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Our function &lt;code&gt;onPanStart&lt;/code&gt; could look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;onPanStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clientY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// get our own transform matrix from the main element
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="c1"&gt;// (we will modify this as we move our mouse along)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTransform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getTransform&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// get the transform matrix used to convert from the SVG element&amp;#39;s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="c1"&gt;// coordinates to the screen/viewport coordinates
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sctm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;main&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;getScreenCTM&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mouseStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;transformFromViewportToElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clientY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sctm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentTransform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;mouseStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTransform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;sctm&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;canvas-wrapper&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mousemove&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPan&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;canvas-wrapper&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mouseup&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPanEnd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In a nutshell:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We get the &lt;code&gt;clientX&lt;/code&gt; and &lt;code&gt;clientY&lt;/code&gt; representing the point in the viewport where the event (&lt;code&gt;mousedown&lt;/code&gt;) occured.&lt;/li&gt;
&lt;li&gt;Then we get the transform matrix of our &lt;code&gt;main&lt;/code&gt; group (in the beginning &lt;code&gt;[ 1, 0, 0, 1, 0, 0 ]&lt;/code&gt;) which we later use to translate/scale. The &lt;code&gt;getTransform&lt;/code&gt; function simply reads the &lt;code&gt;transform&lt;/code&gt; attribute and creates an array from it:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getTransform&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;main&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;transform&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^matrix\(/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\)$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;Also from our &lt;code&gt;main&lt;/code&gt; SVG element, we get the matrix that transforms the element&amp;rsquo;s coordinate system to the viewport&amp;rsquo;s coordinate system.&lt;/li&gt;
&lt;li&gt;Now comes the interesting part: we transform our coordinates from the event into coordinates for the element&amp;rsquo;s coordinate system (&lt;code&gt;transformFromViewportToElement&lt;/code&gt; function explained below).&lt;/li&gt;
&lt;li&gt;Finally, we store the values in variable named &lt;code&gt;canvas&lt;/code&gt; (&lt;code&gt;let canvas = {}&lt;/code&gt;) for later access and add the event listeners for the actual panning (&lt;code&gt;mousemove&lt;/code&gt;) and end (&lt;code&gt;mouseup&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All the magic happens in the &lt;code&gt;transformFromViewportToElement&lt;/code&gt; function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;transformFromViewportToElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sctm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elementTransform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Transforms coordinates from the client (viewport) coordinate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="c1"&gt;// system to coordinates in the SVG element&amp;#39;s coordinate system.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="c1"&gt;// Call this, for example, with clientX and clientY from mouse event.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// create a new DOM point based on coordinates from client viewport
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;DOMPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// get the transform matrix used to convert from the SVG element&amp;#39;s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="c1"&gt;// coordinates to the screen/viewport coordinate system
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;screenTransform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sctm&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;screenTransform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;main&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;getScreenCTM&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;screenTransform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sctm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// now invert it, so we can transform from screen/viewport to element
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inverseScreenTransform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;screenTransform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inverse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// transform the point using the inverted matrix
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transformedPoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matrixTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inverseScreenTransform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// adjust the point for the currently applied scale on the element
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elementTransform&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;transformedPoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="nx"&gt;elementTransform&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// scale x
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="nx"&gt;transformedPoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="nx"&gt;elementTransform&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// scale y
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transformedPoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transformedPoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I commented each step in the function above. Essentially, we create a point with our event coordinates, take the matrix used to convert from the element to the viewport, invert it (since we want the opposite) and then do a matrix transform of our point with said matrix. In case we are working with a transformed element (as is the case for our canvas panning, but not for object dragging) and are in a zoomed-in/out state, we also want to apply &lt;em&gt;that&lt;/em&gt; scale to our coordinate.&lt;/p&gt;
&lt;h2 id="actual-panning-and-ending"&gt;Actual panning and ending&lt;/h2&gt;
&lt;p&gt;To complete our basic example, let&amp;rsquo;s see how the &lt;code&gt;onPan&lt;/code&gt; and &lt;code&gt;onPanEnd&lt;/code&gt; functions could look like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;onPan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clientY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;transformFromViewportToElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clientY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sctm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// calculate how much we have moved from the starting point
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;movement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mouseStart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mouseStart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// set `tx` and `ty` (translate x, y) of matrix with the offset that
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="c1"&gt;// was set at the beginning of the movement minus the actual movement.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startMatrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;startMatrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;startMatrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;movement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;startMatrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;startMatrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;movement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// update the actual transform attribute of the SVG `main` group
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;main&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;transform&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`matrix(&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;startMatrix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;, &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;onPanEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;canvas-wrapper&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mousemove&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPan&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;canvas-wrapper&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mouseup&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPanEnd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the &lt;code&gt;onPan&lt;/code&gt; function we get and transform the new event coordinates (where the mouse pointer is now) in the same way as we did before, then calculate the difference between start and current position, apply the difference to the original (at start of pan) transform matrix&amp;rsquo;s &lt;code&gt;tx&lt;/code&gt; and &lt;code&gt;ty&lt;/code&gt; (index 4 and 5), and finally set the &lt;code&gt;transform&lt;/code&gt; attribute of &lt;code&gt;main&lt;/code&gt; to the updated matrix.&lt;/p&gt;
&lt;div class="notice notice-danger"&gt;
 Note that we are changing the matrix of our &lt;code&gt;transform&lt;/code&gt; attribute of the &lt;em&gt;group&lt;/em&gt; here – this is not the matrix obtained by &lt;code&gt;getScreenCTM&lt;/code&gt;!
&lt;/div&gt;

&lt;p&gt;In &lt;code&gt;onPanEnd&lt;/code&gt; we simply reset our global &lt;code&gt;canvas&lt;/code&gt; variable and then remove the listeners.&lt;/p&gt;
&lt;h2 id="dragging-and-dropping-objects"&gt;Dragging and dropping objects&lt;/h2&gt;
&lt;p&gt;The dragging of objects could be implemented in the same way (using &lt;code&gt;transformFromViewportToElement&lt;/code&gt;). The only difference here is that we would modify the x and y coordinates (or rather &lt;code&gt;cx&lt;/code&gt; and &lt;code&gt;cy&lt;/code&gt;) directly (instead of a &lt;code&gt;transform&lt;/code&gt; attribute), and also ignore the current scale of the &lt;em&gt;group element&amp;rsquo;s&lt;/em&gt; transform matrix (set &lt;code&gt;elementTransform&lt;/code&gt; parameter of &lt;code&gt;transformFromViewportToElement&lt;/code&gt; to null).&lt;/p&gt;</description></item><item><title>Remote debugging Claris Data API</title><link>https://davidhamann.de/2022/05/04/remote-debugging-claris-filemaker-data-api/</link><pubDate>Wed, 04 May 2022 00:00:00 +0000</pubDate><guid>https://davidhamann.de/2022/05/04/remote-debugging-claris-filemaker-data-api/</guid><description>&lt;p&gt;When debugging code that integrates with the Claris FileMaker Data API, it is sometimes helpful to trace a request from your app all the way to the code of the Data API. You might be getting an unexpected error response, want to see what data actually arrives on the server, how a wrapper library, if you use any, might translate the request/response, etc.&lt;/p&gt;
&lt;p&gt;Doing this kind of troubleshooting is much easier when you are able to directly attach a debugger to the remote process. This blog post describes a few steps you can take to remotely debug Data API requests.&lt;/p&gt;
&lt;div class="notice notice-warning"&gt;
 &lt;strong&gt;Start here!&lt;/strong&gt; As the Data API is a NodeJS application, we need a typical remote debugging setup for JavaScript. Please read my last post on &lt;a href="https://davidhamann.de/2022/04/19/remote-debugging-nodejs-apps"&gt;Remote Debugging NodeJS apps&lt;/a&gt; first, if you haven&amp;rsquo;t already done so. We will use the exact same tools in this post.
&lt;/div&gt;

&lt;h2 id="preparing-the-start-script"&gt;Preparing the start script&lt;/h2&gt;
&lt;div class="notice notice-info"&gt;
 I&amp;rsquo;m using a FileMaker Server on macOS for this article. If you want to follow along on a different OS, make sure to use the pathes that correspond to your platform.
&lt;/div&gt;

&lt;p&gt;The first thing we want to do on the FileMaker Server is to stop the Data API with &lt;code&gt;fmsadmin stop fmdapi&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, we need to do a slight modification in a shell script responsible for starting the Data API. Remember from the above linked blog post that we want to start the Inspector service along with the target NodeJS app. The place where this happens is in &lt;code&gt;/Library/FileMaker Server/Web Publishing/publishing-engine/node-wip/wipdeploy.sh&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# [...]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;INSTALL_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/Library/FileMaker Server&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;WIPNODE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$INSTALL_ROOT&lt;/span&gt;&lt;span class="s2"&gt;/node&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;NODE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WIPNODE_PATH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/node&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# [...]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;WIP_NODE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$INSTALL_ROOT&lt;/span&gt;&lt;span class="s2"&gt;/Web Publishing/publishing-engine/node-wip&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;WIP_NODEJS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$WIP_NODE_DIR&lt;/span&gt;&lt;span class="s2"&gt;/app.js&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# [...]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;start&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Starting WIP node&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-p&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;WIPNODE_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Execute node JS &lt;/span&gt;&lt;span class="nv"&gt;$NODE_PATH&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$WIP_NODEJS&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$NODE_PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$WIP_NODEJS&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-&lt;/span&gt;&lt;span class="nv"&gt;$WIP_COMP&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you see in the script, a couple of variables are set to define the install location and app file. Eventually, these variables make up the command to launch the Data API.&lt;/p&gt;
&lt;p&gt;To tell NodeJS that we want to have a debugging endpoint listening we can add our extra argument &lt;code&gt;--inspect&lt;/code&gt; (make sure to set the IP for your environment):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$NODE_PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --inspect&lt;span class="o"&gt;=&lt;/span&gt;172.16.157.133:9229 &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$WIP_NODEJS&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-&lt;/span&gt;&lt;span class="nv"&gt;$WIP_COMP&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With this change in place (save your changes!) we can launch the Data API again, just like we normally would: &lt;code&gt;fmsadmin start fmdapi&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="preparing-our-local-environment"&gt;Preparing our local environment&lt;/h2&gt;
&lt;p&gt;As described in the first blog post we need to have a copy of the source code of the remote app on our local machine as well. We can get this copy of the Data API NodeJS app from &lt;code&gt;/Library/FileMaker Server/Web Publishing/publishing-engine/node-wip&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Opening the code with VSCodium on our local machine, the only thing left to do is to create a &lt;code&gt;.vscode/launch.json&lt;/code&gt; configuration file and set the IP address of the FileMaker Server/Data API host and the assigned UUID:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
 &amp;#34;version&amp;#34;: &amp;#34;0.2.0&amp;#34;,
 &amp;#34;configurations&amp;#34;: [
 {
 &amp;#34;request&amp;#34;: &amp;#34;attach&amp;#34;,
 &amp;#34;name&amp;#34;: &amp;#34;FMS Data API Debugging&amp;#34;,
 &amp;#34;skipFiles&amp;#34;: [
 &amp;#34;&amp;lt;node_internals&amp;gt;/**&amp;#34;
 ],
 &amp;#34;address&amp;#34;: &amp;#34;172.16.157.133:9229/18de71ea-bc0a-4a15-8886-e3c1d24b8e72&amp;#34;,
 &amp;#34;localRoot&amp;#34;: &amp;#34;${workspaceFolder}&amp;#34;,
 &amp;#34;remoteRoot&amp;#34;: &amp;#34;/Library/FileMaker Server/Web Publishing/publishing-engine/node-wip/&amp;#34;,
 &amp;#34;type&amp;#34;: &amp;#34;pwa-node&amp;#34;
 }
 ]
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above config is the same as described in the first blog post. I obtained the UUID via &lt;code&gt;curl http://172.16.157.133:9229/json/list&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="start-debugging"&gt;Start debugging&lt;/h2&gt;
&lt;p&gt;Now that we have all things in place we can start the debugger in VSCodium and it should connect and attach to the Data API app on the remote host:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Debugging Data API Code" loading="lazy" src="https://davidhamann.de/images/data_api_remote_debug_breakpoint.jpg"&gt;&lt;/p&gt;
&lt;h2 id="two-caveats"&gt;Two caveats&lt;/h2&gt;
&lt;p&gt;There are two additional things worth noting in this setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Please be aware of the warning in the &lt;a href="https://davidhamann.de/2022/04/19/remote-debugging-nodejs-apps"&gt;first article&lt;/a&gt; regarding remote code execution, i.e. make sure to have a trusted environment and secure connection to your debugging endpoint.&lt;/li&gt;
&lt;li&gt;While this setup gets you quite far in helping to understand what data arrives at the Data API application, what the expected format of the requests are, error messages being generated, behaviours not being described in the documentation, etc., it is much harder to analyse what happens as soon as the Data API makes a request to the database server via Thrift (as the database server source is obviously not open). Nevertheless, often times tracing the request until it is directed to Thrift will already aid you in identifying potential issues.&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Remote debugging NodeJS apps</title><link>https://davidhamann.de/2022/04/19/remote-debugging-nodejs-apps/</link><pubDate>Tue, 19 Apr 2022 00:00:00 +0000</pubDate><guid>https://davidhamann.de/2022/04/19/remote-debugging-nodejs-apps/</guid><description>&lt;p&gt;When you want to debug an application in an environment which is hard to replicate locally and/or you cannot install additional software on the machine it is running on, remotely connecting a debugger might be a good option to find out what&amp;rsquo;s going (wr)on(g).&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look how we can remote debug a NodeJS application. I will use VSCodium as the debugging client, but there are certainly other options that work equally fine (you could even use the built-in minimal debugger with &lt;code&gt;node inspect host:port&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="setting-up-the-remote-app"&gt;Setting up the remote app&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s say we have the following minimal web application on our remote machine.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app.js&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Hello World!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Example app listening on port &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In normal scenarios we can run the application with &lt;code&gt;node app.js&lt;/code&gt; and it&amp;rsquo;ll start accepting requests on port 3000.&lt;/p&gt;
&lt;p&gt;But NodeJS makes it easy to also launch a debugging endpoint, so-called Inspector, along with application:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;node --inspect=1.2.3.4:9229 app.js
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above command will bind the Inspector service to the IP &lt;code&gt;1.2.3.4&lt;/code&gt; and port &lt;code&gt;9229&lt;/code&gt;. Replace &lt;code&gt;1.2.3.4&lt;/code&gt; with whatever interface you later want to connect to.&lt;/p&gt;
&lt;div class="notice notice-danger"&gt;
 Note that opening up any debugger to the open world is generally a bad idea as access to the debugger essentially means you can execute any code in that environment. If you are not in a closed test environment where you trust the network and its participants, consider running the Inspector on the loopback address and then use SSH port forwarding for a secure connection.
&lt;/div&gt;

&lt;p&gt;Once the Inspector is running we need one more detail to connect to it: the UUID which is assigned when the process starts and which needs to be known to the connecting client.&lt;/p&gt;
&lt;p&gt;If you don&amp;rsquo;t see it in the output when starting your app, you can connect to the following endpoint (on the interface you specified with &lt;code&gt;--inspect&lt;/code&gt;) to discover it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ curl http://127.0.0.1:9229/json/list

[ {
 ...
 &amp;#34;id&amp;#34;: &amp;#34;618339fa-2a18-4fea-b1f5-c20522e17a2b&amp;#34;,
 &amp;#34;title&amp;#34;: &amp;#34;app.js&amp;#34;,
 &amp;#34;type&amp;#34;: &amp;#34;node&amp;#34;,
 &amp;#34;webSocketDebuggerUrl&amp;#34;: &amp;#34;ws://127.0.0.1:9229/618339fa-2a18-4fea-b1f5-c20522e17a2b&amp;#34;
} ]
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="setting-up-the-local-debugger"&gt;Setting up the local debugger&lt;/h2&gt;
&lt;p&gt;Back at our local machine we have three requirements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We need a debugging client (I&amp;rsquo;m using VSCodium)&lt;/li&gt;
&lt;li&gt;We need a copy of the source code of the application to debug&lt;/li&gt;
&lt;li&gt;We need the address and UUID of the Inspector running on the remote machine&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;rsquo;s start by opening the folder with the source code (in this example with only the &lt;code&gt;app.js&lt;/code&gt; file in it) in VSCodium.&lt;/p&gt;
&lt;p&gt;Next, we create a &amp;ldquo;launch configuration&amp;rdquo; so that our debugger knows where and how to connect.&lt;/p&gt;
&lt;p&gt;By default, the configuration will sit in &lt;code&gt;.vscode/launch.json&lt;/code&gt;. For our sample app we use the following contents:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;version&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.2.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;configurations&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pwa-node&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;request&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;attach&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Sample app&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;skipFiles&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;node_internals&amp;gt;/**&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;address&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1.2.3.4:9229/618339fa-2a18-4fea-b1f5-c20522e17a2b&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;localRoot&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;${workspaceFolder}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;remoteRoot&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/path/on/remote/machine/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Essentially, we are saying that we want to &lt;strong&gt;attach&lt;/strong&gt; to an existing process, define the address of the remote machine (including UUID), and point to the directories where the code resides locally and remotely.&lt;/p&gt;
&lt;p&gt;Make sure to adjust the values for the keys &lt;code&gt;address&lt;/code&gt; and &lt;code&gt;remoteRoot&lt;/code&gt; to fit your setup.&lt;/p&gt;
&lt;h2 id="start-debugging"&gt;Start debugging&lt;/h2&gt;
&lt;p&gt;With the launch configuration in place we can connect to the remote process by clicking on the &amp;ldquo;Run and Debug&amp;rdquo; button or just press &lt;code&gt;F5&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Connect debugging client to remote machine" loading="lazy" src="https://davidhamann.de/images/nodejs_remote_debug.png"&gt;&lt;/p&gt;
&lt;p&gt;With the client connected to the remote process, we can start setting breakpoints in the code, send a request (e.g. &lt;code&gt;curl http://1.2.3.4:3000/&lt;/code&gt;) and then (as usual) use the debugger to inspect variables, step through the code, etc.:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Setting a breakpoint in the code" loading="lazy" src="https://davidhamann.de/images/nodejs_remote_debug_breakpoint.png"&gt;&lt;/p&gt;</description></item></channel></rss>