I wanted to draw a rectangle in android, but not just by specifying the left, top, right and bottom. What I have are 4 vertex coordinates. The rectangle is not horizontal but oblique, so something like the image of the rectangle below:
I've been trying to see if I can use a matrix to perform some kind of rotation or use canvas.rotate(), but I'm still not clear how to do it. Can somebody help me with how to draw such a rectangle?
I'd be interested in seeing somebody else's solution to this problem, but if there isn't an easier way, here's how I implemented it the hard way:
public void drawLineThick(GL10 gl, int thickness, FloatBuffer whichBuffer)
{
gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
final float x0 = whichBuffer.get(0);
final float x1 = whichBuffer.get(2);
final float y0 = whichBuffer.get(1);
final float y1 = whichBuffer.get(3);
boolean slopeZeroCase = false;
boolean undefinedSlopeCase = false;
boolean slopeOneCase = false;
boolean slopeNegOneCase = false;
boolean slopeSmall = false;
boolean slopeBig = false;
float boxThickness = thickness * .001f;
//Slope (y2-y1)/(x2-x1)
float m = 0.0f;
//b parameter of y=mx+b formula, b=y-mx
float b = 0.0f;
//Special cases for handling when the slope is zero, undefined
//both (line of length zero), one, or negative one.
if (y1 - y0 == 0)
{
slopeZeroCase = true;
}
if (x1 - x0 == 0)
{
undefinedSlopeCase = true;
}
//If the slope isn't going to be zero or undefined, it's safe to
//actually try to calculate it so that we don't have a "divide by zero"
//by accident
if (slopeZeroCase == false && undefinedSlopeCase == false)
{
m = ((y1 - y0)/(x1 - x0));
b = (y0 - (m*x0));
}
if ( m == 1.0f)
{
slopeOneCase = true;
}
if (m == -1.0f)
{
slopeNegOneCase = true;
}
if ((m > 0 && m < 1) || (m < 0 && m > -1))
{
slopeSmall = true;
}
else
{
slopeBig = true;
}
//float tempFloat[] = new float[8];
//Normal line where there is a slope involved
if (slopeZeroCase == false && undefinedSlopeCase == false && slopeOneCase == false && slopeNegOneCase == false && slopeSmall == true)
{
/**
* Given a sloped line, in order to make it, "thicker",
* one must offset the original line by + and - the
* thickness, in essence creating a box. The formula
* for the points of a given box below (in the direction drawn)
* will be:
*
* p0 p1
* *----------*
* | |
* | |
* | |
* | |
* *----------*
* p3 p2
*
*/
//p1, x
tempFloat[0] = x0;
//p1, y
tempFloat[1] = y0 + boxThickness;
//p2, x
tempFloat[2] = x1;
//p2, y
tempFloat[3] = y1 + boxThickness;
//p3, x
tempFloat[4] = x1;
//p3, y
tempFloat[5] = y1 - boxThickness;
//p4, x
tempFloat[6] = x0;
//p4, y
tempFloat[7] = y0 - boxThickness;
}
else if (slopeZeroCase == false && undefinedSlopeCase == false && slopeOneCase == false && slopeNegOneCase == false && slopeSmall == false)
{
/**
* Given a sloped line, in order to make it, "thicker",
* one must offset the original line by + and - the
* thickness, in essence creating a box. The formula
* for the points of a given box below (in the direction drawn)
* will be:
*
*
*
* p0 p1
* *----------*
* | |
* | |
* | |
* | |
* *----------*
* p3 p2
*
*/
//p1, x
tempFloat[0] = x0 + boxThickness;
//p1, y
tempFloat[1] = y0;
//p2, x
tempFloat[2] = x1 + boxThickness;
//p2, y
tempFloat[3] = y1;
//p3, x
tempFloat[4] = x1 - boxThickness;
//p3, y
tempFloat[5] = y1;
//p4, x
tempFloat[6] = x0 - boxThickness;
//p4, y
tempFloat[7] = y0;
}
//Horizontal line case, only need to change the y to be +- thickness
else if (slopeZeroCase == true && undefinedSlopeCase == false && slopeOneCase == false && slopeNegOneCase == false)
{
//Log.i("draw", "Horizontal");
//p1, x
tempFloat[0] = x0;
//p1, y
tempFloat[1] = y0 + boxThickness;
//p2, x
tempFloat[2] = x1;
//p2, y
tempFloat[3] = y1 + boxThickness;
//p3, x
tempFloat[4] = x1;
//p3, y
tempFloat[5] = y1 - boxThickness;
//p4, x
tempFloat[6] = x0;
//p4, y
tempFloat[7] = y0 - boxThickness;
}
//Vertical line case, only need to change the x to be +- thickness
else if (slopeZeroCase == false && undefinedSlopeCase == true && slopeOneCase == false && slopeNegOneCase == false)
{
//Log.i("draw", "Vertical");
//p1, x
tempFloat[0] = x0 + boxThickness;
//p1, y
tempFloat[1] = y0;
//p2, x
tempFloat[2] = x1 + boxThickness;
//p2, y
tempFloat[3] = y1;
//p3, x
tempFloat[4] = x1 - boxThickness;
//p3, y
tempFloat[5] = y1;
//p4, x
tempFloat[6] = x0 - boxThickness;
//p4, y
tempFloat[7] = y0;
}
//Case where slope = 1
else if (slopeZeroCase == false && undefinedSlopeCase == false && slopeOneCase == true && slopeNegOneCase == false)
{
//Log.i("draw", "OneSlope");
//p1, x
tempFloat[0] = y0 - boxThickness;
//p1, y
tempFloat[1] = x0 + boxThickness;
//p2, x
tempFloat[2] = y1 - boxThickness;
//p2, y
tempFloat[3] = x1 + boxThickness;
//p3, x
tempFloat[4] = y1 + boxThickness;
//p3, y
tempFloat[5] = x1 - boxThickness;
//p4, x
tempFloat[6] = y0 + boxThickness;
//p4, y
tempFloat[7] = x0 - boxThickness;
}
//Case where slope = -1
else if (slopeZeroCase == false && undefinedSlopeCase == false && slopeOneCase == false && slopeNegOneCase == true)
{
Log.i("draw", "OneSlope");
//p1, x
tempFloat[0] = -y0 + boxThickness;
//p1, y
tempFloat[1] = -x0 + boxThickness;
//p2, x
tempFloat[2] = -y1 + boxThickness;
//p2, y
tempFloat[3] = -x1 + boxThickness;
//p3, x
tempFloat[4] = -y1 - boxThickness;
//p3, y
tempFloat[5] = -x1 - boxThickness;
//p4, x
tempFloat[6] = -y0 - boxThickness;
//p4, y
tempFloat[7] = -x0 - boxThickness;
}
//Allocate the wrapped buffers that OpenGL ES uses for drawing, buffers changed to
//be allocated at class scope so that they're not re-allocated every time this
//algorithm is run
//ByteBuffer tempBoxByteBuffer = ByteBuffer.allocateDirect(tempFloat.length * 4);
//tempBoxByteBuffer.order(ByteOrder.nativeOrder());
//FloatBuffer boxFloatBuffer = tempBoxByteBuffer.asFloatBuffer();
boxFloatBuffer.put(tempFloat);
boxFloatBuffer.position(0);
//Draw triangles using points p0, p1, and p2 for the first, and
//p0, p3, and p2 for the second, filling in between. See box
//above for diagram of points. Indices also changed to be allocated at class scope
//short indices[] = {0, 1, 2, 0, 3, 2};
//ByteBuffer tempIndiceBuffer = ByteBuffer.allocateDirect(indices.length * 2);
//tempIndiceBuffer.order(ByteOrder.nativeOrder());
//ShortBuffer indiceBuffer = tempIndiceBuffer.asShortBuffer();
//indiceBuffer.put(indices);
//indiceBuffer.position(0);
gl.glVertexPointer(2, GL10.GL_FLOAT, 0, boxFloatBuffer);
//gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 2);
gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_SHORT, indiceBuffer);
}
This particular implementation was done so that I could have a filled-in box, which is not a default OpenGL ES capability. What I did, instead, was draw two triangles. The math includes several special cases, a zero slope, a one slope, an undefined slope, a slope is greater than 0 but less than one or less than zero but greater than negative one case, or a slope is greater than one or less than negative one case. All you have to do is draw a line loop instead of a triangle between each set of points, and you'll have a non-filled box.
workaround :
instead of drawing rectangle draw a line but set stroke width for paint.
Related
Thanks in advance for your help first.
I have found so many examples using Gyroscope. But I couldn't find adequate one for me.
I'd like to make a simple quiz game that do actions when I tilt VM to 90 degrees forward and backward. Many examples said I might use "pitch" value of Gyroscope. Could you give some advices for me??
I have done a similar thing where i need draw a rectangle with includes nearby places and must point it to the place and show details.
public void onSensorChanged(SensorEvent event) {
final Handler handler = new Handler();
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
mAcceleromterReading =
SensorUtilities.filterSensors(event.values, mAcceleromterReading);
break;
case Sensor.TYPE_MAGNETIC_FIELD:
mMagnetometerReading =
SensorUtilities.filterSensors(event.values, mMagnetometerReading);
break;
float[] orientation =
SensorUtilities.computeDeviceOrientation(mAcceleromterReading, mMagnetometerReading);
if (orientation != null) {
float azimuth = (float) Math.toDegrees(orientation[0]);
if (azimuth < 0) {
azimuth += 360f;
}
// Convert pitch and roll from radians to degrees
float pitch = (float) Math.toDegrees(orientation[1]);
float roll = (float) Math.toDegrees(orientation[2]);
if (abs(pitch - pitchPrev) > PITCH_THRESHOLD && abs(roll - rollPrev) > ROLL_THRESHOLD
&& abs(azimuth - azimuthPrev) > AZIMUTH_THRESHOLD) { // && abs(roll - rollPrev) > rollThreshold
if (DashplexManager.getInstance().mlocation != null) {
mOverlayDisplayView.setHorizontalFOV(mPreview.getHorizontalFOV());
mOverlayDisplayView.setVerticalFOV(mPreview.getVerticalFOV());
mOverlayDisplayView.setAzimuth(azimuth);
mOverlayDisplayView.setPitch(pitch);
mOverlayDisplayView.setRoll(roll);
// Update the OverlayDisplayView to red raw when sensor dataLogin changes,
// redrawing only when the camera is not pointing straight up or down
if (pitch <= 75 && pitch >= -75) {
//Log.d("issueAR", "invalidate: ");
mOverlayDisplayView.invalidate();
}
}
pitchPrev = pitch;
rollPrev = roll;
azimuthPrev = azimuth;
}
}
computeDeviceOrientation method
public static float[] computeDeviceOrientation(float[] accelerometerReading, float[] magnetometerReading) {
if (accelerometerReading == null || magnetometerReading == null) {
return null;
}
final float[] rotationMatrix = new float[9];
SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading);
// Remap the coordinates with the camera pointing along the Y axis.
// This way, portrait and landscape orientation return the same azimuth to magnetic north.
final float cameraRotationMatrix[] = new float[9];
SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_X,
SensorManager.AXIS_Z, cameraRotationMatrix);
final float[] orientationAngles = new float[3];
SensorManager.getOrientation(cameraRotationMatrix, orientationAngles);
// Return a float array containing [azimuth, pitch, roll]
return orientationAngles;
}
onDraw method
#SuppressLint("DrawAllocation")
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Log.d("issueAR", "onDraw: ");
// Log.d("issueAR", "mVerticalFOV: "+mVerticalFOV+" "+"mHorizontalFOV"+mHorizontalFOV);
// Get the viewports only once
if (!mGotViewports && mVerticalFOV > 0 && mHorizontalFOV > 0) {
mViewportHeight = canvas.getHeight() / mVerticalFOV;
mViewportWidth = canvas.getWidth() / mHorizontalFOV;
mGotViewports = true;
//Log.d("onDraw", "mViewportHeight: " + mViewportHeight);
}
if (!mGotViewports) {
return;
}
// Set the paints that remain constant only once
if (!mSetPaints) {
mTextPaint.setTextAlign(Paint.Align.LEFT);
mTextPaint.setTextSize(getResources().getDimensionPixelSize(R.dimen.canvas_text_size));
mTextPaint.setColor(Color.WHITE);
mOutlinePaint.setStyle(Paint.Style.STROKE);
mOutlinePaint.setStrokeWidth(mOutline);
mBubblePaint.setStyle(Paint.Style.FILL);
mSetPaints = true;
}
// Center of view
float x = canvas.getWidth() / 2;
float y = canvas.getHeight() / 2;
/*
* Uncomment line below to allow rotation of display around the center point
* based on the roll. However, this "feature" is not very intuitive, and requires
* locking device orientation to portrait or changes the sensor rotation matrix
* on device rotation. It's really quite a nightmare.
*/
//canvas.rotate((0.0f - mRoll), x, y);
float dy = mPitch * mViewportHeight;
if (mNearbyPlaces != null) {
//Log.d("OverlayDisplayView", "mNearbyPlaces: "+mNearbyPlaces.size());
// Iterate backwards to draw more distant places first
for (int i = mNearbyPlaces.size() - 1; i >= 0; i--) {
NearbyPlace nearbyPlace = mNearbyPlaces.get(i);
float xDegreesToTarget = mAzimuth - nearbyPlace.getBearingToPlace();
float dx = mViewportWidth * xDegreesToTarget;
float iconX = x - dx;
float iconY = y - dy;
if (isOverlapping(iconX, iconX).isOverlapped()) {
PointF point = calculateNewXY(new PointF(iconX, iconY + mViewportHeight));
iconX = point.x;
iconY = point.y;
}
nearbyPlace.setIconX(iconX);
nearbyPlace.setIconY(iconY);
Bitmap icon = getIcon(nearbyPlace.getIcon_id());
float width = icon.getWidth() + mTextPaint.measureText(nearbyPlace.getName()) + mMargin;
RectF recf=new RectF(iconX, iconY, width, icon.getHeight());
nearbyPlace.setRect(recf);
float angleToTarget = xDegreesToTarget;
if (xDegreesToTarget < 0) {
angleToTarget = 360 + xDegreesToTarget;
}
if (angleToTarget >= 0 && angleToTarget < 90) {
nearbyPlace.setQuadrant(1);
mQuad1Places.add(nearbyPlace);
} else if (angleToTarget >= 90 && angleToTarget < 180) {
nearbyPlace.setQuadrant(2);
mQuad2Places.add(nearbyPlace);
} else if (angleToTarget >= 180 && angleToTarget < 270) {
nearbyPlace.setQuadrant(3);
mQuad3Places.add(nearbyPlace);
} else {
nearbyPlace.setQuadrant(4);
mQuad4Places.add(nearbyPlace);
}
//Log.d("TAG", " - X: " + iconX + " y: " + iconY + " angle: " + angleToTarget + " display: " + nearbyPlace.getIcon_id());
}
drawQuadrant(mQuad1Places, canvas);
drawQuadrant(mQuad2Places, canvas);
drawQuadrant(mQuad3Places, canvas);
drawQuadrant(mQuad4Places, canvas);
}
}
It doesnot contain full code, but you may understand how pitch and azimuth with roll is used.. Best of luck
This is my code so far:
local lines = {}
local lineGroup = display.newGroup()
local prevX,prevY
local isDrawing = false
local i = 0
local function distanceBetween(x1, y1, x2, y2)
local dist_x = x2 - x1
local dist_y = y2 - y1
local distanceBetween = math.sqrt((dist_x*dist_x) + (dist_y*dist_y))
return distanceBetween
end
local function drawLine(e)
if(e.phase == "began") then
prevX = e.x
prevY = e.y
isDrawing = true
i = i + 1
elseif(e.phase == "moved") then
local distance = distanceBetween(prevX, prevY, e.x, e.y)
if(isDrawing and distance < 100) then
if(lines[i]) then lineGroup:remove(i) end
lines[i] = display.newLine(prevX, prevY, e.x, e.y)
lines[i]:setStrokeColor(1,45,1)
lines[i].strokeWidth = 5
local dist_x = e.x - prevX
local dist_y = e.y - prevY
physics.addBody(lines[i], "static", { density = 1, friction = 0.5, bounce = 2, shape = {0, 0, dist_x, dist_y, 0, 0} } )
lineGroup:insert(lines[i])
end
elseif(e.phase == "ended") then
isDrawing = false
end
end
Runtime:addEventListener("touch",drawLine)
And, how do I get one line to disappear when another is drawn?
If you want to disappear all other lines in your lineGroup while drawing a new line, you can do it simply as follows:
Comment your line:
if(lines[i]) then lineGroup:remove(i) end
And add the following lines inside e.phase == "began"
lineGroup:removeSelf()
lineGroup = nil
lineGroup = display.newGroup()
Keep Coding........... :)
I am creating a game which involves shooting in the direction where user clicked.
So from point A(x,y) to point B(x1,y1) i want the bullet bitmap to animate and i have done some calculation/math and figured out some way to do it, but it's not that great-looking doesn't feel so natural.
My approach to doing this is calculate the difference between x and x1 and y and y1 and just scale it.
In example if the X difference between x and x1 is 100 and Y difference between y and y1 i calculate X/Y and get 2.0 which is equal to 2:1 so I know that I should move X two times faster than Y.
Here is my code, if anyone has any suggestions how to make it better, let me know.
float proportion;
float diffX = (x1 - x);
if(diffX == 0) diffX = 0.00001f;
float diffY = (y1 - y);
if(diffY == 0) diffY = 0.00001f;
if(Math.abs(diffX)>Math.abs(diffY)){
proportion = Math.abs(diffX)/Math.abs(diffY);
speedY = 2;
speedX = proportion * speedY;
}
else if(Math.abs(diffX)<Math.abs(diffY)){
proportion = Math.abs(diffY)/Math.abs(diffX);
speedX = 2;
speedY = proportion * speedX;
}
else{
speedX = speedY = 2;
}
if(diffY<0) speedY = -speedY;
if(diffX<0) speedX = -speedX;
if(speedX>=10) speedX = 9;
if(speedX<=-10) speedX = -9;
if(speedY>=10) speedY = 9;
if(speedY<=-10) speedY = -10;
The following implements LERP (linear interpolation) to move you along a straight line.
// move from (x1, y1) to (x2,y2) with speed "speed" (that must be positive)
final double deltay = y2-y1;
final double deltax = x2-x1;
double deltalen = sqrt(deltay*deltay + deltax*deltax);
if (deltalen <= speed)
display(x2, y2);
else {
double finalx = x1 + deltax * speed/deltalen; // surely deltalen > 0, since speed >=0
double finaly = y1 + deltay * speed/deltalen;
display(finalx, finaly);
}
Here is the code to elaborate on my comment:
float slope = (x2 -x1)/(y2 - y1);
float dx = 0.1f; // tweake to set bullet's speed
float x = x1;
while(x < x2)
{
float y = slope*(x - x1) + y1;
DisplayBullet(x, y);
x += dx;
}
// at this point x = x2 and, if everything went right, y = y2
Here I'm assuming that x1 < x2. You'll have to swap points when that's not the case.
I want the user to be able to drag the edges of a square around the canvas. With my current solution it works but has glitches, sometimes an edge cannot be selected. Is there a clean way to tell if a line has been clicked (e.g. passes through a coordinate)? This is how I'm currently testing:
// check edge pressed, edge is the line between to
// coords e.g. (i) & (i = 1)
for (int i = 0; i < coords.size(); i++) {
p1 = coords.get(i);
if ((i + 1) > (coords.size() - 1)) p2 = coords.get(0);
else p2 = coords.get(i + 1);
// is this the line pressed
if (p1.x <= event.getX() + 5 && event.getX() - 5 <= p2.x && p1.y <= event.getY() + 5 && event.getY() - 5 <= p2.y) {
// points found, set to non temp
// variable for use in ACTION_MOVE
point1 = p1;
point2 = p2;
break;
} else if (p1.x >= event.getX() + 5 && event.getX() - 5 >= p2.x && p1.y >= event.getY() + 5 && event.getY() - 5 >= p2.y) {
// points found, set to non temp
// variable for use in ACTION_MOVE
point1 = p1;
point2 = p2;
break;
}
}
The code bellow //is this the line pressed is the most important and also most likely the issue. The +5 and -5 are used to give the use a larger area to click on.
Here is the whole on click event:
public void EditEdge() {
//TODO this works like shit
// Detect the two coordinates along the edge pressed and drag
// them
scene.setOnTouchListener(new View.OnTouchListener() {
private int startX;
private int startY;
private Point point1 = new Point(0, 0);
private Point point2 = new Point(0, 0);
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) event.getX();
startY = (int) event.getY();
Point p1;
Point p2;
// check edge pressed, edge is the line between to
// coords e.g. (i) & (i = 1)
for (int i = 0; i < coords.size(); i++) {
p1 = coords.get(i);
if ((i + 1) > (coords.size() - 1)) p2 = coords.get(0);
else p2 = coords.get(i + 1);
// is this the line pressed
if (p1.x <= event.getX() + 5 && event.getX() - 5 <= p2.x && p1.y <= event.getY() + 5 && event.getY() - 5 <= p2.y) {
// points found, set to non temp
// variable for use in ACTION_MOVE
point1 = p1;
point2 = p2;
break;
} else if (p1.x >= event.getX() + 5 && event.getX() - 5 >= p2.x && p1.y >= event.getY() + 5 && event.getY() - 5 >= p2.y) {
// points found, set to non temp
// variable for use in ACTION_MOVE
point1 = p1;
point2 = p2;
break;
}
}
break;
case MotionEvent.ACTION_UP:
point1 = new Point(0, 0);
point2 = new Point(0, 0);
// scene.setOnTouchListener(scene.editModeOnTouchListener);
break;
case MotionEvent.ACTION_MOVE:
for (Point p: new Point[] {
point1, point2
}) {
int modX = (int)(p.x + (event.getX() - startX));
int modY = (int)(p.y + (event.getY() - startY));
p.set(modX, modY);
}
SetCoords(coords);
startX = (int) event.getX();
startY = (int) event.getY();
break;
default:
return false;
}
return true;
}
});
}
So is there a easier way to tell is a line is clicked/ passes through a point or is that not the issue?
Thanks
use the line equation y = mx + b to find out if the point is on a line
float EPSILON = 0.001f;
public boolean isPointOnLine(Point linePointA, Point linePointB, Point point) {
float m = (linePointB.y - linePointA.y) / (linePointB.x - linePointA.x);
float b = linePointA.y - m * linePointA.x;
return Math.abs(point.y - (m*point.x+b)) < EPSILON);
}
Great piece of code by #tyczj !
I added a use-case to handle vertical lines, which gives me the following code fragment:
public boolean isPointOnLine(PointF lineStaPt, PointF lineEndPt, PointF point) {
final float EPSILON = 0.001f;
if (Math.abs(staPt.x - endPt.x) < EPSILON) {
// We've a vertical line, thus check only the x-value of the point.
return (Math.abs(point.x - lineStaPt.x) < EPSILON);
} else {
float m = (lineEndPt.y - lineStaPt.y) / (lineEndPt.x - lineStaPt.x);
float b = lineStaPt.y - m * lineStaPt.x;
return (Math.abs(point.y - (m * point.x + b)) < EPSILON);
}
}
Also a piece of code to check if a point lies on a line-segment:
public boolean isPointOnLineSegment(PointF staPt, PointF endPt, PointF point) {
final float EPSILON = 0.001f;
if (isPointOnLine(staPt, endPt, point)) {
// Create lineSegment bounding-box.
RectF lb = new RectF(staPt.x, staPt.y, endPt.x, endPt.y);
// Extend bounds with epsilon.
RectF bounds = new RectF(lb.left - EPSILON, lb.top - EPSILON, lb.right + EPSILON, lb.bottom + EPSILON);
// Check if point is contained within lineSegment-bounds.
return bounds.contains(point.x, point.y);
}
return false;
}
You could define 8 Rect to check against - the 4 sides and 4 corners (so you can move 2 edges at once). The lines of the edge should have a width for the touchable area.
Define a Point centred on your touch event, there are then methods for checking if a rect contains a point.
Seems like there should be some convenient way to do this?
I couldn't find one, so I threw together the below algorithm. Is it memory/computationally optimal?
Thanks:
Edit: Original algorithm was stupidly wrong, maybe this is better?
public static float minDistance(RectF rect, PointF point)
{
if(rect.contains(point.x, point.y))
{
//North line
float distance = point.y - rect.top;
//East line
distance = Math.min(distance, point.x - rect.left);
//South line
distance = Math.min(distance, rect.bottom - point.y);
//West line
distance = Math.min(distance, rect.right - point.x);
return distance;
}
else
{
float minX, minY;
if (point.x < rect.left)
{
minX = rect.left;
}
else if (point.x > rect.right)
{
minX = rect.right;
}
else
{
minX = point.x;
}
if (point.y < rect.top)
{
minY = rect.top;
}
else if (point.y > rect.bottom)
{
minY = rect.bottom;
}
else
{
minY = point.y;
}
float vectorX = point.x - minX;
float vectorY = point.y - minY;
float distance = (float) Math.sqrt((vectorX * vectorX) + (vectorY * vectorY));
return distance;
}
}
Just take the closest point and then get the distance to that.
Off the top of my head:
float closestX, closestY;
if(point.x >= x1 && point.x <= x2 && point.y >= y1 && point.y <= y2)
{
float bestDistance = point.y - y1;
bestDistance = Math.min(distance, y2 - point.y);
bestDistance = Math.min(distance, point.x - x1);
bestDistance = Math.min(distance, x2 - point.x);
return bestDistance;
}
if (point.x < x1) {
closestX = x1;
} else if (point.x > x2) {
closestX = x2;
} else {
closestX = point.x;
}
if (point.y < x1) {
closestY = y1;
} else if (point.y > y2) {
closestY = y2;
} else {
closestY = point.y;
}
float vectorY = point.x - closestX;
float vectorY = point.Y - closestY;
float distance = sqrtf((vectorX * vectorX) + (vectorY * vectorY));
One optimization is to not use the square root until the end. If you just compare distance squared and then return the sqrt of the smallest distance squared, you only have to do one sqrt.
Edit: Here is a good example of the distance from a point to a line segment (edge of the rect). You can use it, and modify it so that it returns the distance squared instead. Then compare them all and return the sqrt of the min distance squared.
Distance Between Point and Segment
The natural approach is to consider the eight areas outside the square, four corners and four laterals. This gives the shortest distance possible to the border of the square. If the point is inside the square (perhaps a button) then distance is zero, but if a distance is required is the shortest straight to the four borders.