Scale at pivot point in an already scaled node

后端 未结 4 1910
北荒
北荒 2020-12-05 01:19

I\'m trying to create an application with a zoomable/pannable canvas.

Features:

  • zoom in/out with mouse wheel at pivot points
相关标签:
4条回答
  • 2020-12-05 01:43

    In FX8 you can do

    final Affine accumulatedScales = new Affine();  
    chart.getTransforms().add(accumulatedScales);  
    
    chart.setOnScroll(new EventHandler<ScrollEvent>() {  
        @Override public void handle(ScrollEvent event) {  
            accumulatedScales.appendScale(scaleFactor, scaleFactor, event.getX(), event.getY());  
        }  
    });
    

    and you are done

    0 讨论(0)
  • 2020-12-05 01:43

    With this approach you can zoom in position with transformed, rotated nodes.

        public static void zoom(Node node, double factor, ScrollEvent event) {
    
                double oldScale = node.getScaleX();
                double scale = oldScale*factor;
                if (scale < MIN_ZOOM) scale = MIN_ZOOM;
                if (scale > MAX_ZOOM)  scale = MAX_ZOOM;
    
                double x = event.getX();
                double y = event.getY();
    
                Point2D p0 = node.localToScene(x, y);
    
                node.setScaleX(scale);
                node.setScaleY(scale);
    
                Point2D p1 = node.localToScene(x, y);
    
                double deltaX = p1.getX() - p0.getX();
                double deltaY = p1.getY() - p0.getY();
    
                node.setTranslateX(node.getTranslateX() - deltaX);
                node.setTranslateY(node.getTranslateY() - deltaY);
    
            }
    
    0 讨论(0)
  • 2020-12-05 01:45

    I change your SceneGestures class so now working.

    class SceneGestures {
        private double oldx;
        private double oldy;
        double ttx=0;
        double tty=0;
        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<MouseEvent> getOnMousePressedEventHandler() {
            return onMousePressedEventHandler;
        }
    
        public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
            return onMouseDraggedEventHandler;
        }
    
        public EventHandler<ScrollEvent> getOnScrollEventHandler() {
            return onScrollEventHandler;
        }
    
        private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
    
            public void handle(MouseEvent event) {
    
                // right mouse button => panning
                if( !event.isSecondaryButtonDown())
                    return;
    
                sceneDragContext.mouseAnchorX = event.getSceneX();
                sceneDragContext.mouseAnchorY = event.getSceneY();
    
                sceneDragContext.translateAnchorX = canvas.getTranslateX();
                sceneDragContext.translateAnchorY = canvas.getTranslateY();
    
            }
    
        };
    
        private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
            public void handle(MouseEvent event) {
    
                // right mouse button => 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<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
    
            @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() < 0)
                    scale -= delta;
                else
                    scale += delta;
    
                if (scale <= MIN_SCALE) {
                    scale = MIN_SCALE;
                } else if (scale >= MAX_SCALE) {
                    scale = MAX_SCALE;
                }
    
    
    
                if (oldx==0){
                 ttx=event.getSceneX() ;  
                 tty=event.getSceneY() ;
                }else{
                if (oldx!=event.getSceneX()){
                ttx=((event.getSceneX()+oldx)/2) ;    
                }
                if (oldy!=event.getSceneY()){
                tty=((event.getSceneY()+oldy)/2);    
                }
                }
                // pivot value must be untransformed, i. e. without scaling
                canvas.setPivot( 
                        ((ttx- canvas.getBoundsInParent().getMinX()) / oldScale),
                        ((tty- canvas.getBoundsInParent().getMinY()) / oldScale)
                        );
             //    if (oldx==0){
                oldx=event.getSceneX();
                oldy=event.getSceneY();
             //   }
                //try {
                 //   Robot rbt=new Robot();
                 //   rbt.mouseMove(512, 384);
                //} catch (AWTException ex) {
                //    System.out.println(ex.getMessage());
               // }
    
                 canvas.setScale( scale);
    
    
                System.out.println( "new pivot x: " + canvas.scaleTransform.getPivotX() + "/" + canvas.scaleTransform.getPivotY() + ", new scale: " + scale);
                System.out.println( "bounds: " + canvas.getBoundsInParent());       
                System.out.println( "old: " + oldx+"  "+oldy);  
                System.out.println( "tt: " + ttx+"  "+tty);  
                event.consume();
    
            }
    
        };
    
    
    }
    
    0 讨论(0)
  • 2020-12-05 01:50

    First I would recommend to not scale in linear steps but by factors to smooth the scaling:

               double delta = 1.2;
               if (event.getDeltaY() < 0)
                    scale /= delta;
                else
                    scale *= delta;
    

    ... and to be somehow bossy, I recommend curly brackets as a good style ;-) :

                   double delta = 1.2;
                   if (event.getDeltaY() < 0) {
                        scale /= delta;
                   } else {
                        scale *= delta;
                   }
    

    ... and to use the mouse scroll value for even better quality:

                   double delta = 1.2;
                   if (event.getDeltaY() < 0) {
                        scale /= Math.pow(delta, -event.getDeltaY()/20);
                   } else {
                        scale *= Math.pow(delta, event.getDeltaY()/20);
                   }
    

    ... that is finally the same as:

                   scale *= Math.pow(1.01, event.getDeltaY());
    

    Second I recomend to use the canvas translate and scale properties instead of a Transformation:

    public class ZoomApplication extends Application {
        static public class PannableCanvas extends Pane {
    
            DoubleProperty myScale = new SimpleDoubleProperty(1.0);
    
            public PannableCanvas() {
    
                setPrefSize(600, 600);
                setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
    
                // add scale transform
                scaleXProperty().bind(myScale);
                scaleYProperty().bind(myScale);
    
                // logging
                addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { 
                    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 < w; i+=offset) {
                    // vertical
                    gc.strokeLine( i, 0, i, h);
                    // horizontal
                    gc.strokeLine( 0, i, w, i);
                }
    
                getChildren().add( grid);
    
                grid.toBack();
            }
    
            public double getScale() {
                return myScale.get();
            }
    
            /**
             * Set x/y scale
             * @param myScale
             */
            public void setScale( double scale) {
                myScale.set(scale);
            }
    
            /**
             * Set x/y pivot points
             * @param x
             * @param y
             */
            public void setPivot( double x, double y) {
                setTranslateX(getTranslateX()-x);
                setTranslateY(getTranslateY()-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<MouseEvent> getOnMousePressedEventHandler() {
                return onMousePressedEventHandler;
            }
    
            public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
                return onMouseDraggedEventHandler;
            }
    
            private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
    
                public void handle(MouseEvent event) {
    
                    // left mouse button => 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<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
                public void handle(MouseEvent event) {
    
                    // left mouse button => 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<MouseEvent> getOnMousePressedEventHandler() {
                return onMousePressedEventHandler;
            }
    
            public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
                return onMouseDraggedEventHandler;
            }
    
            public EventHandler<ScrollEvent> getOnScrollEventHandler() {
                return onScrollEventHandler;
            }
    
            private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
    
                public void handle(MouseEvent event) {
    
                    // right mouse button => panning
                    if( !event.isSecondaryButtonDown())
                        return;
    
                    sceneDragContext.mouseAnchorX = event.getSceneX();
                    sceneDragContext.mouseAnchorY = event.getSceneY();
    
                    sceneDragContext.translateAnchorX = canvas.getTranslateX();
                    sceneDragContext.translateAnchorY = canvas.getTranslateY();
    
                }
    
            };
    
            private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
                public void handle(MouseEvent event) {
    
                    // right mouse button => 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<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
    
                @Override
                public void handle(ScrollEvent event) {
    
    
                    double scale = canvas.getScale(); // currently we only use Y, same value is used for X
                    double oldScale = scale;
    
                    scale *= Math.pow(1.01, event.getDeltaY());
    
                    if (scale <= MIN_SCALE) {
                        scale = MIN_SCALE;
                    } else if (scale >= MAX_SCALE) {
                        scale = MAX_SCALE;
                    }
    
                    double f = (scale / oldScale)-1;
    
                    double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
                    double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
    
    
                    canvas.setScale( scale);
                    canvas.setPivot(f*dx, f*dy);
    
                    event.consume();
    
                }
    
            };
    
    
        }
    
        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 => 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();
    
        }
    }
    

    After some thoughts about zoom, I came to the conclusion, that it would be a good idea to

    1. Write an independent zoom helper method to ease the zoom function
    2. To also support the pinch-to-zoom gesture with the same method

    So I wrote the following helper method:

    /** Allow to zoom/scale any node with pivot at scene (x,y) coordinates.
     * 
     * @param node
     * @param delta
     * @param x
     * @param y
     */
    public static void zoom(Node node, double factor, double x, double y) {
        double oldScale = node.getScaleX();
        double scale = oldScale * factor;
        if (scale < 0.05) scale = 0.05;
        if (scale > 50)  scale = 50;
        node.setScaleX(scale);
        node.setScaleY(scale);
    
        double  f = (scale / oldScale)-1;
        Bounds bounds = node.localToScene(node.getBoundsInLocal());
        double dx = (x - (bounds.getWidth()/2 + bounds.getMinX()));
        double dy = (y - (bounds.getHeight()/2 + bounds.getMinY()));
    
        node.setTranslateX(node.getTranslateX()-f*dx);
        node.setTranslateY(node.getTranslateY()-f*dy);
    }
    
    public static void zoom(Node node, ScrollEvent event) {
        zoom(node, Math.pow(1.01, event.getDeltaY()), event.getSceneX(), event.getSceneY());
    }
    public static void zoom(Node node, ZoomEvent event) {
        zoom(node, event.getZoomFactor(), event.getSceneX(), event.getSceneY());
    }
    

    allowing me to register zoom function on any node as easy as:

        myView.setOnScroll(event -> GUITools.zoom(myView, event)); // mouse scroll wheel zoom
        myView.setOnZoom(event -> GUITools.zoom(myView, event)); // pinch to zoom
    

    and done...

    0 讨论(0)
提交回复
热议问题