사용자로 하여금 손가락으로 화면을 터치하면서 그림을 그릴 수 있는 실습 프로젝트 해 봅니다.
예전에 연습삼아 만들어 봤던 아주 간단한 앱 입니다.
내 어린 사촌은 이 앱에서 내 iPad로 손가락으로 자유롭게 그림을 그렸습니다(어린이 그림: 원, 선 등등의 마음에 떠오르는 모든 것).
그런 다음 그는 원을 그리기 시작했고 "괜찮은 원"으로 만들어 보라고 말했습니다.(원하는 건 : 원을 완벽하게 둥글기 만들라는 것이죠: 우리가 생각해도, 화면에 손가락으로 뭔가를 그리려고 아무리 고정 시키려고 해도 그려진 원은 생각만큼 둥글지 그려지지 않습니다.)
그래서 제 질문은 코드에서 원을 형성하는 사용자가 그린 선을 먼저 감지하고 화면에서 완벽하게 둥글게 만들어 거의 동일한 크기의 원을 생성할 수 있는 방법이 있을까 하는 것입니다.
그다지 직선이 아닌 선을 직선으로 만드는 것은 할 수 있었지만, 원에 관해서는 Quartz나 다른 방법으로 하는 방법을 잘 모르겠습니다.
사용자가 자신의 손가락을 뗏을 때 선의 시작점과 끝점이 닿거나 서로 교차 되었다는 것은 그가 실제 원을 그리려고 했다는 사실로 판단한다고 생각 했을 때 입니다.
답변:
때로는 처음부터 다시 만들어 보는 데 시간을 투가자 하는 것도 해 볼만한 일입니다.
이미 눈치채셨겠지만 이런 작업을 할 수 있는 많은 프레임워크가 존재 하지만, 복잡하지도 않고 간단하면서도 유용한 솔루션을 구현하는 것은 그리 어렵지 않습니다.
(오해하지 마세요. 목적이 진지하다면 입증된 성숙하고 안정적인 프레임워크를 사용하는 것이 좋습니다.)
나는 먼저 내 결과를 말씀드린 다음, 이에 대한 간단하고 직접적인 아이디어를 설명 하겠습니다.
내 구현은 모든 각각의 위치를 분석해서 복잡한 계산을 수행할 필요가 없음을 볼 수 있을 것입니다.
아이디어는 귀중한 메타 정보를 발견하는 것입니다.
나는 접선을 예로 사용하겠습니다:
선택한 모양에 대해서 일반적으로 사용되는 단순하고 직접적인 패턴을 식별해 봅시다:
그리고 이 아이디어를 기반으로 원 감지 메커니즘을 구현하는 것은 그리 어렵지 않습니다.
아래의 작업 데모를 참조하십시오(죄송합니다. 이 빠르고 다소 지저분한 예제를 제공하는 가장 빠른 방법으로 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;
}
인덱스의 기본값이 사용된다는 점에 유의하십시오.
모양의 사용 가능한 "부분"을 통한 간단한 검색으로 이러한 제한이 제거 될 것입니다.
전체 모양을 감지하려면 원형 버퍼를 사용해야 할 필요가 있다는 것에 유의 하십시오:
시계 방향 및 시계 반대 방향
두 모드를 모두 지원하려면 이전 개선 사항의 순환 버퍼를 사용하고 양방향으로 검색해야 할 것입니다:
타원 그리기
경계 배열(bounds array)에 이미 필요한 모든 것이 준비 되었습니다.
해당 데이터를 사용하기만 하면 됩니다:
cWidth = bounds[2].y - bounds[0].y;
cHeight = bounds[3].y - bounds[1].y;
기타 제스처(선택 사항)
마지막으로 다른 제스처를 지원하려면 dx(또는 dy)가 0인 상황을 적절하게 처리하면 됩니다:
업데이트
이 작은 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);
}
이상.
'프로그래밍' 카테고리의 다른 글
윈도우 용 Gnuplot 빌드해보기 (0) | 2024.02.01 |
---|---|
디자인패턴 커닝지 (0) | 2023.04.21 |
MinGW 정적 동적 라이브러리 (0) | 2023.04.19 |
네이티브 이미지의 동적 프록시 (0) | 2023.04.18 |
언어별 - 프록시 디자인 패턴 (0) | 2023.04.16 |