私には、ユーザーが指でタッチしながら画面に絵を描くことができる練習プロジェクトがあります。これは、昔、練習として作った非常にシンプルなアプリです。私のいとこは、このアプリを使って、私の iPad で指で自由に絵を描きました (子供の絵: 円、線など、思いついたもの)。それから、いとこは円を描き始め、それを「きれいな円」にするように私に頼みました (私の理解では、描いた円を完全な円にすることです。画面に指でどれだけ安定して何かを描こうとしても、円は本来あるべき円にはならないことはわかっているからです)。
そこで私の質問は、ユーザーが描いた円を形成する線を最初に検出し、画面上で完全に円形にすることでほぼ同じサイズの円を生成するコード内の方法はあるかどうかです。それほど直線ではない線をまっすぐにする方法はわかっていますが、円については、Quartz や他の方法でそれを実行する方法がよくわかりません。
私の考えでは、実際に円を描こうとしていたという事実を正当化するには、ユーザーが指を離した後に、線の始点と終点が互いに接触するか交差する必要があるということです。
ベストアンサー1
時には、車輪の再発明に時間を費やすことが本当に役に立つことがあります。すでにお気づきかもしれませんが、フレームワークは数多くありますが、それほど複雑にすることなく、シンプルでありながら便利なソリューションを実装することはそれほど難しくありません。(誤解しないでください。真剣な目的の場合は、成熟していて安定性が実証されているフレームワークを使用する方がよいでしょう)。
まず私の結果を提示し、次にその背後にあるシンプルでわかりやすいアイデアを説明します。
私の実装では、すべてのポイントを分析して複雑な計算を行う必要がないことがわかります。アイデアは、いくつかの貴重なメタ情報を見つけることです。正接例:
選択した形状に典型的な、シンプルでわかりやすいパターンを特定しましょう。
したがって、このアイデアに基づいて円検出メカニズムを実装するのはそれほど難しくありません。以下の動作デモを参照してください (申し訳ありませんが、この高速で少し汚い例を提供するための最速の方法として Java を使用しています)。
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<Point> 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<Point> 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);
}
});
}
}
iOSで同様の動作を実装するのは、いくつかのイベントと座標だけが必要なので、問題にはならないはずです。次のようになります(例):
- (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];
}
いくつかの機能強化が可能です。
どこからでも開始
現在の要件は、次の簡略化により、上部の中央点から円を描き始めることです。
if(type == null || type != newType) {
if(newType != shape[index]) {
break;
}
bounds[index] = current;
detected[index++] = newType;
}
のデフォルト値がindex
使用されていることに注意してください。形状の利用可能な「部分」を簡単に検索すると、この制限は解除されます。完全な形状を検出するには、円形バッファを使用する必要があることに注意してください。
時計回りと反時計回り
両方のモードをサポートするには、以前の拡張機能の循環バッファを使用して両方向に検索する必要があります。
楕円を描く
必要なものはすべてすでにbounds
配列に含まれています。
そのデータを使用するだけです:
cWidth = bounds[2].y - bounds[0].y;
cHeight = bounds[3].y - bounds[1].y;
その他のジェスチャー(オプション)
最後に、他のジェスチャをサポートするには、dx
(または) がゼロに等しい状況を適切に処理する必要があります。dy
アップデート
この小さな PoC はかなりの注目を集めたので、スムーズに動作するようにコードを少し更新し、描画のヒントや強調表示のサポートポイントなどを提供しました。
コードは次のとおりです:
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<Point> 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<Point> 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);
}