title: Scale at pivot point in an already scaled node
body: <p>I'm trying to create an application with a zoomable/pannable canvas.</p>
  
  <p><strong>Features</strong>:</p>
  
  <ul>
  <li>zoom in/out with mouse wheel at pivot points </li>
  <li>drag nodes around on the canvas with left mouse button </li>
  <li>drag the entire canvas with right mouse button</li>
  </ul>
  
  <p>The zooming at the pivot point works as long as you start the zooming at scale 1. Position the mouse over a grid point and scroll the mouse wheel. The pivot point will remain where you started zooming. </p>
  
  <p><strong>Problem</strong>: </p>
  
  <p>When you zoom in, then move the mouse to another point and zoom again, then the pivot point is shifted and zooming isn't happening anymore at the initial mouse position.</p>
  
  <p><strong>Example</strong>:</p>
  
  <p>Here's the code:</p>
  
  <pre><code>import javafx.application.Application;
  import javafx.event.EventHandler;
  import javafx.scene.Group;
  import javafx.scene.Node;
  import javafx.scene.Scene;
  import javafx.scene.canvas.Canvas;
  import javafx.scene.canvas.GraphicsContext;
  import javafx.scene.control.Label;
  import javafx.scene.input.MouseEvent;
  import javafx.scene.input.ScrollEvent;
  import javafx.scene.layout.Pane;
  import javafx.scene.paint.Color;
  import javafx.scene.shape.Circle;
  import javafx.scene.shape.Rectangle;
  import javafx.scene.transform.Scale;
  import javafx.stage.Stage;
  
  /**
   * The canvas which holds all of the nodes of the application.
   */
  class PannableCanvas extends Pane {
  
      Scale scaleTransform;
  
      public PannableCanvas() {
  
          setPrefSize(600, 600);
          setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
  
          // add scale transform
          scaleTransform = new Scale( 1.0, 1.0);
          getTransforms().add( scaleTransform);
  
          // logging
          addEventFilter(MouseEvent.MOUSE_PRESSED, event -&gt; { 
              System.out.println( 
                      "canvas event: " + ( ((event.getSceneX() - getBoundsInParent().getMinX()) / getScale()) + ", scale: " + getScale())
                      );
              System.out.println( "canvas bounds: " + getBoundsInParent());   
                  });
  
      }
  
      /**
       * Add a grid to the canvas, send it to back
       */
      public void addGrid() {
  
          double w = getBoundsInLocal().getWidth();
          double h = getBoundsInLocal().getHeight();
  
          // add grid
          Canvas grid = new Canvas(w, h);
  
          // don't catch mouse events
          grid.setMouseTransparent(true);
  
          GraphicsContext gc = grid.getGraphicsContext2D();
  
          gc.setStroke(Color.GRAY);
          gc.setLineWidth(1);
  
          // draw grid lines
          double offset = 50;
          for( double i=offset; i &lt; w; i+=offset) {
              // vertical
              gc.strokeLine( i, 0, i, h);
              // horizontal
              gc.strokeLine( 0, i, w, i);
          }
  
          getChildren().add( grid);
  
          grid.toBack();
      }
  
      public Scale getScaleTransform() {
          return scaleTransform;
      }
  
      public double getScale() {
          return scaleTransform.getY();
      }
  
      /**
       * Set x/y scale
       * @param scale
       */
      public void setScale( double scale) {
          scaleTransform.setX(scale);
          scaleTransform.setY(scale);
      }
  
      /**
       * Set x/y pivot points
       * @param x
       * @param y
       */
      public void setPivot( double x, double y) {
          scaleTransform.setPivotX(x);
          scaleTransform.setPivotY(y);
      }
  }
  
  
  /**
   * Mouse drag context used for scene and nodes.
   */
  class DragContext {
  
      double mouseAnchorX;
      double mouseAnchorY;
  
      double translateAnchorX;
      double translateAnchorY;
  
  }
  
  /**
   * Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
   */
  class NodeGestures {
  
      private DragContext nodeDragContext = new DragContext();
  
      PannableCanvas canvas;
  
      public NodeGestures( PannableCanvas canvas) {
          this.canvas = canvas;
  
      }
  
      public EventHandler&lt;MouseEvent&gt; getOnMousePressedEventHandler() {
          return onMousePressedEventHandler;
      }
  
      public EventHandler&lt;MouseEvent&gt; getOnMouseDraggedEventHandler() {
          return onMouseDraggedEventHandler;
      }
  
      private EventHandler&lt;MouseEvent&gt; onMousePressedEventHandler = new EventHandler&lt;MouseEvent&gt;() {
  
          public void handle(MouseEvent event) {
  
              // left mouse button =&gt; dragging
              if( !event.isPrimaryButtonDown())
                  return;
  
              nodeDragContext.mouseAnchorX = event.getSceneX();
              nodeDragContext.mouseAnchorY = event.getSceneY();
  
              Node node = (Node) event.getSource();
  
              nodeDragContext.translateAnchorX = node.getTranslateX();
              nodeDragContext.translateAnchorY = node.getTranslateY();
  
          }
  
      };
  
      private EventHandler&lt;MouseEvent&gt; onMouseDraggedEventHandler = new EventHandler&lt;MouseEvent&gt;() {
          public void handle(MouseEvent event) {
  
              // left mouse button =&gt; dragging
              if( !event.isPrimaryButtonDown())
                  return;
  
              double scale = canvas.getScale();
  
              Node node = (Node) event.getSource();
  
              node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
              node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
  
              event.consume();
  
          }
      };
  }
  
  /**
   * Listeners for making the scene's canvas draggable and zoomable
   */
  class SceneGestures {
  
      private static final double MAX_SCALE = 10.0d;
      private static final double MIN_SCALE = .1d;
  
      private DragContext sceneDragContext = new DragContext();
  
      PannableCanvas canvas;
  
      public SceneGestures( PannableCanvas canvas) {
          this.canvas = canvas;
      }
  
      public EventHandler&lt;MouseEvent&gt; getOnMousePressedEventHandler() {
          return onMousePressedEventHandler;
      }
  
      public EventHandler&lt;MouseEvent&gt; getOnMouseDraggedEventHandler() {
          return onMouseDraggedEventHandler;
      }
  
      public EventHandler&lt;ScrollEvent&gt; getOnScrollEventHandler() {
          return onScrollEventHandler;
      }
  
      private EventHandler&lt;MouseEvent&gt; onMousePressedEventHandler = new EventHandler&lt;MouseEvent&gt;() {
  
          public void handle(MouseEvent event) {
  
              // right mouse button =&gt; panning
              if( !event.isSecondaryButtonDown())
                  return;
  
              sceneDragContext.mouseAnchorX = event.getSceneX();
              sceneDragContext.mouseAnchorY = event.getSceneY();
  
              sceneDragContext.translateAnchorX = canvas.getTranslateX();
              sceneDragContext.translateAnchorY = canvas.getTranslateY();
  
          }
  
      };
  
      private EventHandler&lt;MouseEvent&gt; onMouseDraggedEventHandler = new EventHandler&lt;MouseEvent&gt;() {
          public void handle(MouseEvent event) {
  
              // right mouse button =&gt; panning
              if( !event.isSecondaryButtonDown())
                  return;
  
              canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
              canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - sceneDragContext.mouseAnchorY);
  
              event.consume();
          }
      };
  
      /**
       * Mouse wheel handler: zoom to pivot point
       */
      private EventHandler&lt;ScrollEvent&gt; onScrollEventHandler = new EventHandler&lt;ScrollEvent&gt;() {
  
          @Override
          public void handle(ScrollEvent event) {
  
              double delta = 1;
  
              double scale = canvas.getScale(); // currently we only use Y, same value is used for X
              double oldScale = scale;
  
              if (event.getDeltaY() &lt; 0)
                  scale -= delta;
              else
                  scale += delta;
  
              if (scale &lt;= MIN_SCALE) {
                  scale = MIN_SCALE;
              } else if (scale &gt;= MAX_SCALE) {
                  scale = MAX_SCALE;
              }
  
              // pivot value must be untransformed, i. e. without scaling
              canvas.setPivot( 
                      ((event.getSceneX() - canvas.getBoundsInParent().getMinX()) / oldScale),
                      ((event.getSceneY() - canvas.getBoundsInParent().getMinY()) / oldScale)
                      );
  
              canvas.setScale( scale);
  
              System.out.println( "new pivot x: " + canvas.scaleTransform.getPivotX() + "/" + canvas.scaleTransform.getPivotY() + ", new scale: " + scale);
              System.out.println( "bounds: " + canvas.getBoundsInParent());       
  
              event.consume();
  
          }
  
      };
  
  
  }
  
  /**
   * An application with a zoomable and pannable canvas.
   */
  public class ScrollApplication extends Application {
      public static void main(String[] args) {
          launch(args);
      }
  
      @Override
      public void start(Stage stage) {
  
          Group group = new Group();
  
          // create canvas
          PannableCanvas canvas = new PannableCanvas();
  
          // we don't want the canvas on the top/left in this example =&gt; just
          // translate it a bit
          canvas.setTranslateX(100);
          canvas.setTranslateY(100);
  
          // create sample nodes which can be dragged
          NodeGestures nodeGestures = new NodeGestures( canvas);
  
          Label label1 = new Label("Draggable node 1");
          label1.setTranslateX(10);
          label1.setTranslateY(10);
          label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
          label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
  
          Label label2 = new Label("Draggable node 2");
          label2.setTranslateX(100);
          label2.setTranslateY(100);
          label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
          label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
  
          Label label3 = new Label("Draggable node 3");
          label3.setTranslateX(200);
          label3.setTranslateY(200);
          label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
          label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
  
          Circle circle1 = new Circle( 300, 300, 50);
          circle1.setStroke(Color.ORANGE);
          circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
          circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
          circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
  
          Rectangle rect1 = new Rectangle(100,100);
          rect1.setTranslateX(450);
          rect1.setTranslateY(450);
          rect1.setStroke(Color.BLUE);
          rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
          rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
          rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
  
          canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
  
          group.getChildren().add(canvas);
  
          // create scene which can be dragged and zoomed
          Scene scene = new Scene(group, 1024, 768);
  
          SceneGestures sceneGestures = new SceneGestures(canvas);
          scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
          scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
          scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
  
          stage.setScene(scene);
          stage.show();
  
          canvas.addGrid();
  
      }
  }
  </code></pre>
  
  <p>It's obviously something wrong with the pivot point calculation, but I can't figure out what it is and how to fix it.</p>
  
  <p>Thank you very much!</p>
  
  <p><img src="https://i.stack.imgur.com/BkE0i.png" alt="enter image description here"></p>