Draw a perfect circle from user's touch

后端 未结 7 1672
天命终不由人
天命终不由人 2021-01-29 17:28

I have this practice project that allows the user to draw on the screen as they touch with their fingers. Very simple App I did as exercise way back. My little cousin took the l

7条回答
  •  予麋鹿
    予麋鹿 (楼主)
    2021-01-29 17:39

    Sometimes it is really useful to spend some time reinventing the wheel. As you might have already noticed there are a lot of frameworks, but it is not that hard to implement a simple, but yet useful solution without introducing all that complexity. (Please don't get me wrong, for any serious purpose it is better to use some mature and proven to be stable framework).

    I will present my results first and then explain the simple and straightforward idea behind them.

    enter image description here

    You'll see in my implementation there is no need to analyze every single point and do complex computations. The idea is to spot some valuable meta information. I will use tangent as an example:

    enter image description here

    Let's identify a simple and straightforward pattern, typical for the selected shape:

    enter image description here

    So it is not that hard to implement a circle detection mechanism based on that idea. See working demo below (Sorry, I'm using Java as the fastest way to provide this fast and a bit dirty example):

    import java.awt.BasicStroke;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.HeadlessException;
    import java.awt.Point;
    import java.awt.RenderingHints;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseListener;
    import java.awt.event.MouseMotionListener;
    import java.util.ArrayList;
    import java.util.List;
    import javax.swing.JFrame;
    import javax.swing.SwingUtilities;
    
    public class CircleGestureDemo extends JFrame implements MouseListener, MouseMotionListener {
    
        enum Type {
            RIGHT_DOWN,
            LEFT_DOWN,
            LEFT_UP,
            RIGHT_UP,
            UNDEFINED
        }
    
        private static final Type[] circleShape = {
            Type.RIGHT_DOWN,
            Type.LEFT_DOWN,
            Type.LEFT_UP,
            Type.RIGHT_UP};
    
        private boolean editing = false;
        private Point[] bounds;
        private Point last = new Point(0, 0);
        private List points = new ArrayList<>();
    
        public CircleGestureDemo() throws HeadlessException {
            super("Detect Circle");
    
            addMouseListener(this);
            addMouseMotionListener(this);
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            setPreferredSize(new Dimension(800, 600));
            pack();
        }
    
        @Override
        public void paint(Graphics graphics) {
            Dimension d = getSize();
            Graphics2D g = (Graphics2D) graphics;
    
            super.paint(g);
    
            RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            g.setRenderingHints(qualityHints);
    
            g.setColor(Color.RED);
            if (cD == 0) {
                Point b = null;
                for (Point e : points) {
                    if (null != b) {
                        g.drawLine(b.x, b.y, e.x, e.y);
                    }
                    b = e;
                }
            }else if (cD > 0){
                g.setColor(Color.BLUE);
                g.setStroke(new BasicStroke(3));
                g.drawOval(cX, cY, cD, cD);
            }else{
                g.drawString("Uknown",30,50);
            }
        }
    
    
        private Type getType(int dx, int dy) {
            Type result = Type.UNDEFINED;
    
            if (dx > 0 && dy < 0) {
                result = Type.RIGHT_DOWN;
            } else if (dx < 0 && dy < 0) {
                result = Type.LEFT_DOWN;
            } else if (dx < 0 && dy > 0) {
                result = Type.LEFT_UP;
            } else if (dx > 0 && dy > 0) {
                result = Type.RIGHT_UP;
            }
    
            return result;
        }
    
        private boolean isCircle(List points) {
            boolean result = false;
            Type[] shape = circleShape;
            Type[] detected = new Type[shape.length];
            bounds = new Point[shape.length];
    
            final int STEP = 5;
    
            int index = 0;        
            Point current = points.get(0);
            Type type = null;
    
            for (int i = STEP; i < points.size(); i += STEP) {
                Point next = points.get(i);
                int dx = next.x - current.x;
                int dy = -(next.y - current.y);
    
                if(dx == 0 || dy == 0) {
                    continue;
                }
    
                Type newType = getType(dx, dy);
                if(type == null || type != newType) {
                    if(newType != shape[index]) {
                        break;
                    }
                    bounds[index] = current;
                    detected[index++] = newType;
                }
                type = newType;            
                current = next;
    
                if (index >= shape.length) {
                    result = true;
                    break;
                }
            }
    
            return result;
        }
    
        @Override
        public void mousePressed(MouseEvent e) {
            cD = 0;
            points.clear();
            editing = true;
        }
    
        private int cX;
        private int cY;
        private int cD;
    
        @Override
        public void mouseReleased(MouseEvent e) {
            editing = false;
            if(points.size() > 0) {
                if(isCircle(points)) {
                    cX = bounds[0].x + Math.abs((bounds[2].x - bounds[0].x)/2);
                    cY = bounds[0].y;
                    cD = bounds[2].y - bounds[0].y;
                    cX = cX - cD/2;
    
                    System.out.println("circle");
                }else{
                    cD = -1;
                    System.out.println("unknown");
                }
                repaint();
            }
        }
    
        @Override
        public void mouseDragged(MouseEvent e) {
            Point newPoint = e.getPoint();
            if (editing && !last.equals(newPoint)) {
                points.add(newPoint);
                last = newPoint;
                repaint();
            }
        }
    
        @Override
        public void mouseMoved(MouseEvent e) {
        }
    
        @Override
        public void mouseEntered(MouseEvent e) {
        }
    
        @Override
        public void mouseExited(MouseEvent e) {
        }
    
        @Override
        public void mouseClicked(MouseEvent e) {
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new Runnable() {
    
                @Override
                public void run() {
                    CircleGestureDemo t = new CircleGestureDemo();
                    t.setVisible(true);
                }
            });
        }
    }
    

    It should not be a problem to implement similar behavior on iOS, since you just need several events and coordinates. Something like the following (see example):

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        UITouch* touch = [[event allTouches] anyObject];
    }
    
    - (void)handleTouch:(UIEvent *)event {
        UITouch* touch = [[event allTouches] anyObject];
        CGPoint location = [touch locationInView:self];
    
    }
    
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
        [self handleTouch: event];
    }
    
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        [self handleTouch: event];    
    }
    

    There are several enhancements possible.

    Start at any point

    Current requirement is to start drawing a circle from the top middle point due to the following simplification:

            if(type == null || type != newType) {
                if(newType != shape[index]) {
                    break;
                }
                bounds[index] = current;
                detected[index++] = newType;
            }
    

    Please notice the default value of index is used. A simple search through the available "parts" of the shape will remove that limitation. Please note you'll need to use a circular buffer in order to detect a full shape:

    enter image description here

    Clockwise and counterclockwise

    In order to support both modes you will need to use the circular buffer from the previous enhancement and search in both directions:

    enter image description here

    Draw an ellipse

    You have everything you need already in the bounds array.

    enter image description here

    Simply use that data:

    cWidth = bounds[2].y - bounds[0].y;
    cHeight = bounds[3].y - bounds[1].y;
    

    Other gestures (optional)

    Finally, you just need to properly handle a situation when dx (or dy) is equal to zero in order to support other gestures:

    enter image description here

    Update

    This small PoC got quite a high attention, so I did update the code a bit in order to make it work smoothly and provide some drawing hints, highlight supporting points, etc:

    Here is the code:

    import java.awt.BasicStroke;
    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.HeadlessException;
    import java.awt.Point;
    import java.awt.RenderingHints;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseListener;
    import java.awt.event.MouseMotionListener;
    import java.util.ArrayList;
    import java.util.List;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.SwingUtilities;
    
    public class CircleGestureDemo extends JFrame {
    
        enum Type {
    
            RIGHT_DOWN,
            LEFT_DOWN,
            LEFT_UP,
            RIGHT_UP,
            UNDEFINED
        }
    
        private static final Type[] circleShape = {
            Type.RIGHT_DOWN,
            Type.LEFT_DOWN,
            Type.LEFT_UP,
            Type.RIGHT_UP};
    
        public CircleGestureDemo() throws HeadlessException {
            super("Circle gesture");
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            setLayout(new BorderLayout());
            add(BorderLayout.CENTER, new GesturePanel());
            setPreferredSize(new Dimension(800, 600));
            pack();
        }
    
        public static class GesturePanel extends JPanel implements MouseListener, MouseMotionListener {
    
            private boolean editing = false;
            private Point[] bounds;
            private Point last = new Point(0, 0);
            private final List points = new ArrayList<>();
    
            public GesturePanel() {
                super(true);
                addMouseListener(this);
                addMouseMotionListener(this);
            }
    
            @Override
            public void paint(Graphics graphics) {
                super.paint(graphics);
    
                Dimension d = getSize();
                Graphics2D g = (Graphics2D) graphics;
    
                RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
                qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    
                g.setRenderingHints(qualityHints);
    
                if (!points.isEmpty() && cD == 0) {
                    isCircle(points, g);
                    g.setColor(HINT_COLOR);
                    if (bounds[2] != null) {
                        int r = (bounds[2].y - bounds[0].y) / 2;
                        g.setStroke(new BasicStroke(r / 3 + 1));
                        g.drawOval(bounds[0].x - r, bounds[0].y, 2 * r, 2 * r);
                    } else if (bounds[1] != null) {
                        int r = bounds[1].x - bounds[0].x;
                        g.setStroke(new BasicStroke(r / 3 + 1));
                        g.drawOval(bounds[0].x - r, bounds[0].y, 2 * r, 2 * r);
                    }
                }
    
                g.setStroke(new BasicStroke(2));
                g.setColor(Color.RED);
    
                if (cD == 0) {
                    Point b = null;
                    for (Point e : points) {
                        if (null != b) {
                            g.drawLine(b.x, b.y, e.x, e.y);
                        }
                        b = e;
                    }
    
                } else if (cD > 0) {
                    g.setColor(Color.BLUE);
                    g.setStroke(new BasicStroke(3));
                    g.drawOval(cX, cY, cD, cD);
                } else {
                    g.drawString("Uknown", 30, 50);
                }
            }
    
            private Type getType(int dx, int dy) {
                Type result = Type.UNDEFINED;
    
                if (dx > 0 && dy < 0) {
                    result = Type.RIGHT_DOWN;
                } else if (dx < 0 && dy < 0) {
                    result = Type.LEFT_DOWN;
                } else if (dx < 0 && dy > 0) {
                    result = Type.LEFT_UP;
                } else if (dx > 0 && dy > 0) {
                    result = Type.RIGHT_UP;
                }
    
                return result;
            }
    
            private boolean isCircle(List points, Graphics2D g) {
                boolean result = false;
                Type[] shape = circleShape;
                bounds = new Point[shape.length];
    
                final int STEP = 5;
                int index = 0;
                int initial = 0;
                Point current = points.get(0);
                Type type = null;
    
                for (int i = STEP; i < points.size(); i += STEP) {
                    final Point next = points.get(i);
                    final int dx = next.x - current.x;
                    final int dy = -(next.y - current.y);
    
                    if (dx == 0 || dy == 0) {
                        continue;
                    }
    
                    final int marker = 8;
                    if (null != g) {
                        g.setColor(Color.BLACK);
                        g.setStroke(new BasicStroke(2));
                        g.drawOval(current.x - marker/2, 
                                   current.y - marker/2, 
                                   marker, marker);
                    }
    
                    Type newType = getType(dx, dy);
                    if (type == null || type != newType) {
                        if (newType != shape[index]) {
                            break;
                        }
                        bounds[index++] = current;
                    }
    
                    type = newType;
                    current = next;
                    initial = i;
    
                    if (index >= shape.length) {
                        result = true;
                        break;
                    }
                }
                return result;
            }
    
            @Override
            public void mousePressed(MouseEvent e) {
                cD = 0;
                points.clear();
                editing = true;
            }
    
            private int cX;
            private int cY;
            private int cD;
    
            @Override
            public void mouseReleased(MouseEvent e) {
                editing = false;
                if (points.size() > 0) {
                    if (isCircle(points, null)) {
                        int r = Math.abs((bounds[2].y - bounds[0].y) / 2);
                        cX = bounds[0].x - r;
                        cY = bounds[0].y;
                        cD = 2 * r;
                    } else {
                        cD = -1;
                    }
                    repaint();
                }
            }
    
            @Override
            public void mouseDragged(MouseEvent e) {
                Point newPoint = e.getPoint();
                if (editing && !last.equals(newPoint)) {
                    points.add(newPoint);
                    last = newPoint;
                    repaint();
                }
            }
    
            @Override
            public void mouseMoved(MouseEvent e) {
            }
    
            @Override
            public void mouseEntered(MouseEvent e) {
            }
    
            @Override
            public void mouseExited(MouseEvent e) {
            }
    
            @Override
            public void mouseClicked(MouseEvent e) {
            }
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new Runnable() {
    
                @Override
                public void run() {
                    CircleGestureDemo t = new CircleGestureDemo();
                    t.setVisible(true);
                }
            });
        }
    
        final static Color HINT_COLOR = new Color(0x55888888, true);
    }
    

提交回复
热议问题