TileProvider graphics gets skewed on higher zoom levels - android

Im currently playing with TileProver in Android Maps API v2 and kinda stuck with the following problem: graphics which I paint manually into Bitmap gets skewed significantly on higher zoom levels:
Let me explain what Im doing here. I have number of LatLng points and I draw a circle for every point on a map, so as you zoom in - point stays at the same geo location. As you can see on the screenshot, circles look fine on lower zoom levels, but as you start zooming in - circles get skewed..
That's how it is implemented:
package trickyandroid.com.locationtracking;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.Log;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import java.io.ByteArrayOutputStream;
/**
* Created by paveld on 8/8/14.
*/
public class CustomTileProvider implements TileProvider {
private final int TILE_SIZE = 256;
private int density = 1;
private int tileSizeScaled;
private Paint circlePaint;
private SphericalMercatorProjection projection;
private Point[] points;
public CustomTileProvider(Context context) {
density = 3; //hardcoded for now, but should be driven by DisplayMetrics.density
tileSizeScaled = TILE_SIZE * density;
projection = new SphericalMercatorProjection(TILE_SIZE);
points = generatePoints();
circlePaint = new Paint();
circlePaint.setAntiAlias(true);
circlePaint.setColor(0xFF000000);
circlePaint.setStyle(Paint.Style.FILL);
}
private Point[] generatePoints() {
Point[] points = new Point[6];
points[0] = projection.toPoint(new LatLng(47.603861, -122.333393));
points[1] = projection.toPoint(new LatLng(47.600389, -122.326741));
points[2] = projection.toPoint(new LatLng(47.598942, -122.318973));
points[3] = projection.toPoint(new LatLng(47.599000, -122.311549));
points[4] = projection.toPoint(new LatLng(47.601373, -122.301721));
points[5] = projection.toPoint(new LatLng(47.609764, -122.311850));
return points;
}
#Override
public Tile getTile(int x, int y, int zoom) {
Bitmap bitmap = Bitmap.createBitmap(tileSizeScaled, tileSizeScaled, Bitmap.Config.ARGB_8888);
float scale = (float) (Math.pow(2, zoom) * density);
Matrix m = new Matrix();
m.setScale(scale, scale);
m.postTranslate(-x * tileSizeScaled, -y * tileSizeScaled);
Canvas c = new Canvas(bitmap);
c.setMatrix(m);
for (Point p : points) {
c.drawCircle((float) p.x, (float) p.y, 20 / scale, circlePaint);
}
return bitmapToTile(bitmap);
}
private Tile bitmapToTile(Bitmap bmp) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] bitmapdata = stream.toByteArray();
return new Tile(tileSizeScaled, tileSizeScaled, bitmapdata);
}
}
Logic tells me that this is happening because I'm translating LatLng into screen position only for 1 tile (256x256 which is zoom level 0) and then in order to translate this screen point to other zoom levels, I need to scale my bitmap and translate it to appropriate position. At the same time, since bitmap is scaled, I need to compensate circle radius, so I divide radius by scale factor. So at zoom level 19 my scale factor is already 1572864 which is huge. It is like looking at this circle via huge magnifying glass. That's why I have this effect.
So I suppose the solution would be to avoid bitmap scaling and scale/translate only screen coordinates. In this case my circle radius will be always the same and will not be downscaled.
Unfortunately, matrix math is not my strongest skill, so my question is - how do I scale/translate set of points for arbitrary zoom level having set of points calculated for zoom level '0'?
The easiest way for doing this would be to have different Projection instances for each zoom level, but since GeoPoint -> ScreenPoint translation is quite expensive operation, I would keep this approach as a back-up and use some simple math for translating already existing screen points.
NOTE
Please note that I need specifically custom TileProvider since in the app I will be drawing much more complicated tiles than just circles. So simple Marker class is not going to work for me here
UPDATE
Even though I figured out how to translate individual points and avoid bitmap scaling:
c.drawCircle((float) p.x * scale - (x * tileSizeScaled), (float) p.y * scale - (y * tileSizeScaled), 20, circlePaint);
I still don't know how to do this with Path objects. I cannot translate/scale path like you would do this with individual points, so I still have to scale my bitmap which causes drawing artifacts again (stroke width is skewed on higher zoom levels):
And here is a code snippet:
package trickyandroid.com.locationtracking;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import java.io.ByteArrayOutputStream;
/**
* Created by paveld on 8/8/14.
*/
public class CustomTileProvider implements TileProvider {
private final int TILE_SIZE = 256;
private int density = 1;
private int tileSizeScaled;
private SphericalMercatorProjection projection;
private Point[] points;
private Path path;
private Paint pathPaint;
public CustomTileProvider(Context context) {
density = 3; //hardcoded for now, but should be driven by DisplayMetrics.density
tileSizeScaled = TILE_SIZE * density;
projection = new SphericalMercatorProjection(TILE_SIZE);
points = generatePoints();
path = generatePath(points);
pathPaint = new Paint();
pathPaint.setAntiAlias(true);
pathPaint.setColor(0xFF000000);
pathPaint.setStyle(Paint.Style.STROKE);
pathPaint.setStrokeCap(Paint.Cap.ROUND);
pathPaint.setStrokeJoin(Paint.Join.ROUND);
}
private Path generatePath(Point[] points) {
Path path = new Path();
path.moveTo((float) points[0].x, (float) points[0].y);
for (int i = 1; i < points.length; i++) {
path.lineTo((float) points[i].x, (float) points[i].y);
}
return path;
}
private Point[] generatePoints() {
Point[] points = new Point[10];
points[0] = projection.toPoint(new LatLng(47.603861, -122.333393));
points[1] = projection.toPoint(new LatLng(47.600389, -122.326741));
points[2] = projection.toPoint(new LatLng(47.598942, -122.318973));
points[3] = projection.toPoint(new LatLng(47.599000, -122.311549));
points[4] = projection.toPoint(new LatLng(47.601373, -122.301721));
points[5] = projection.toPoint(new LatLng(47.609764, -122.311850));
points[6] = projection.toPoint(new LatLng(47.599221, -122.311531));
points[7] = projection.toPoint(new LatLng(47.599663, -122.312410));
points[8] = projection.toPoint(new LatLng(47.598823, -122.312614));
points[9] = projection.toPoint(new LatLng(47.599959, -122.310651));
return points;
}
#Override
public Tile getTile(int x, int y, int zoom) {
Bitmap bitmap = Bitmap.createBitmap(tileSizeScaled, tileSizeScaled, Bitmap.Config.ARGB_8888);
float scale = (float) (Math.pow(2, zoom) * density);
Canvas c = new Canvas(bitmap);
Matrix m = new Matrix();
m.setScale(scale, scale);
m.postTranslate(-x * tileSizeScaled, -y * tileSizeScaled);
c.setMatrix(m);
pathPaint.setStrokeWidth(6 * density / scale);
c.drawPath(path, pathPaint);
return bitmapToTile(bitmap);
}
private Tile bitmapToTile(Bitmap bmp) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] bitmapdata = stream.toByteArray();
return new Tile(tileSizeScaled, tileSizeScaled, bitmapdata);
}
}

I see you are using google's tileview, you can try and consider mogarius's library which is an open source on github
I never tried it before but it support most of the functionalities you need (markers\dots and dynamic path drawing) out of the box so that'll save you the time spending on making matrix calculations for upscaling and downscaling.
there is also a demo video for some of the usage he made, and a great javadoc he published.

Related

Google Vision: Drawing mask on Face with animations

I am using google vision library for face detection. Face detection is perfect and I get all the info like vertices, angles like eulerY, eulerZ.
I want to draw mask on face, drawing is ok but the face mask is not following the face position as it should, the position is not correct. Here is my edited code to draw face mask on googly eyes project.
Here is my source code:
package com.google.android.gms.samples.vision.face.googlyeyes;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.google.android.gms.samples.vision.face.googlyeyes.ui.camera.GraphicOverlay;
import com.google.android.gms.vision.face.Face;
import java.util.HashMap;
/**
* Graphics class for rendering Googly Eyes on a graphic overlay given the current eye positions.
*/
class GooglyEyesGraphic extends GraphicOverlay.Graphic {
private Paint mEyeWhitesPaint;
private Paint mEyeIrisPaint;
private Paint mEyeOutlinePaint;
private Paint mEyeLidPaint;
Paint mBoxPaint;
Context mContext;
private static final float BOX_STROKE_WIDTH = 20.0 f;
FrameLayout frameLayout;
ImageView imageView;
// Bitmap bmpOriginal;
//==============================================================================================
// Methods
//==============================================================================================
GooglyEyesGraphic(GraphicOverlay overlay, Context mContext) {
super(overlay);
this.mContext = mContext;
mEyeWhitesPaint = new Paint();
mEyeWhitesPaint.setColor(Color.WHITE);
mEyeWhitesPaint.setStyle(Paint.Style.FILL);
mEyeLidPaint = new Paint();
mEyeLidPaint.setColor(Color.YELLOW);
mEyeLidPaint.setStyle(Paint.Style.FILL);
mEyeIrisPaint = new Paint();
mEyeIrisPaint.setColor(Color.BLACK);
mEyeIrisPaint.setStyle(Paint.Style.FILL);
mEyeOutlinePaint = new Paint();
mEyeOutlinePaint.setColor(Color.BLACK);
mEyeOutlinePaint.setStyle(Paint.Style.STROKE);
mEyeOutlinePaint.setStrokeWidth(5);
mBoxPaint = new Paint();
mBoxPaint.setColor(Color.MAGENTA);
mBoxPaint.setStyle(Paint.Style.STROKE);
mBoxPaint.setStrokeWidth(BOX_STROKE_WIDTH);
mBoxPaint.setAlpha(40);
LayoutInflater li = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = li.inflate(R.layout.mask_layout, null);
imageView = (ImageView) view.findViewById(R.id.flMaskIV);
frameLayout = (FrameLayout) view.findViewById(R.id.frameLayout);
}
private volatile Face mFace;
/**
* Updates the eye positions and state from the detection of the most recent frame. Invalidates
* the relevant portions of the overlay to trigger a redraw.
*/
void updateEyes(PointF leftPosition, boolean leftOpen,
PointF rightPosition, boolean rightOpen, Face mFace) {
if (facesList.containsKey(mFace.getId())) {
PointF pointF1 = facesList.get(mFace.getId()).getPosition();
PointF pointF2 = mFace.getPosition();
double x = Math.sqrt(Math.pow(pointF2.x - pointF1.x, 2) - Math.pow(pointF2.y - pointF1.y, 2));
if (x < 0)
x = (-1 * x);
if (x < 10)
return;
Log.e("face Called", "FaceCalled");
}
this.mFace = mFace;
facesList.put(mFace.getId(), mFace);
postInvalidate();
}
public HashMap < Integer, Face > facesList = new HashMap < > ();
/**
* Draws the current eye state to the supplied canvas. This will draw the eyes at the last
* reported position from the tracker, and the iris positions according to the physics
* simulations for each iris given motion and other forces.
*/
#Override
public void draw(Canvas canvas) {
if (mFace == null)
return;
// if (facesList.containsKey(mFace.getId())) {
// PointF pointF1 = facesList.get(mFace.getId()).getPosition();
// PointF pointF2 = mFace.getPosition();
//
// double x = Math.sqrt(Math.pow(pointF2.x - pointF1.x, 2) - Math.pow(pointF2.y - pointF1.y, 2));
// if (x < 0)
// x = (-1 * x);
// if (x < 10)
// return;
// Log.e("face Called", "FaceCalled");
//
// }
//
// facesList.put(mFace.getId(), mFace);
if (this.canvas == null)
this.canvas = canvas;
applyMask();
}
Drawable drawable;
Canvas canvas;
private void applyMask() {
if (canvas == null)
return;
// Log.e("mFace.getEulerY()", "mFace.getEulerY()=> " + mFace.getEulerY());
if (GooglyEyesActivity.maskImgView != null) {
GooglyEyesActivity.maskImgView.setVisibility(View.GONE);
GooglyEyesActivity.maskImgView.setImageResource(GooglyEyesActivity.currEmoticonID);
}
float x = translateX(mFace.getPosition().x + mFace.getWidth() / 2);
float y = translateY(mFace.getPosition().y + mFace.getHeight() / 2);
// Draws a bounding box around the face.
float xOffset = scaleX(mFace.getWidth() / 2.0 f);
float yOffset = scaleY(mFace.getHeight() / 2.0 f);
float left = x - xOffset - 50;
float top = y - (yOffset) - 50;
float right = x + xOffset + 50;
float bottom = y + (yOffset) + 50;
// canvas.drawRect((int) left, (int) top, (int) right, (int) bottom, mBoxPaint);
drawable = GooglyEyesActivity.maskImgView.getDrawable();
///////////////////
canvas.save();
canvas.translate(left, top);
// frameLayout.setX(left);
// frameLayout.setY(top);
Rect rect = new Rect((int) left, (int) top, (int) right, (int) bottom);
frameLayout.measure(rect.width(), rect.height());
frameLayout.setLayoutParams(new LinearLayout.LayoutParams(rect.width(), rect.height()));
frameLayout.layout(0, 0, (int) right, (int) bottom);
frameLayout.setClipBounds(rect);
imageView.setLayoutParams(new FrameLayout.LayoutParams(rect.width(), rect.height()));
imageView.setRotationY(mFace.getEulerY());
imageView.setRotation(mFace.getEulerZ());
imageView.setImageDrawable(drawable);
frameLayout.draw(canvas);
canvas.restore();
}
}
Also i need to add animations so i tried using dlib library to get landmarks points and draw it using opengl but in opengl i dont have any function to populate the vertice array i am getting from dlib. As the dlib landmarks are in points but the array there is not in such a way. Any help will be appreciated for both scenarios.
Thank you in advance.
Thanks.

How to draw upscaled pixel art efficiently in Android?

I'm making a game that has lots of pixel art in it. A standard button in my game is about 12x12 pixels and scaled up five times for displaying. All of the characters are also scaled two times bigger, depending on the screen. The problem is that if I store all these images to upscaled bitmaps, I get the out of memory error in Android. If I try to scale them up every time for drawing, the game runs really slowly.
That's why I wrote my own PixelArtBitmap class. It results in great images and requires so little memory, but is highly inefficient, dropping my fps a lot. It's still faster than scaling bitmaps up every time for drawing. Here's the class:
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
class PixelArtBitmap{
private int[][] pixels = null;
private boolean[][] visible = null;
private float width = 0;
private float height = 0;
private float drawWidth = 0;
private float drawHeight = 0;
private int pixelsX = 0;
private int pixelsY = 0;
private static Paint boxPaint = new Paint();
public PixelArtBitmap(Bitmap bitmap, float scale){
boxPaint.setStrokeWidth(0);
pixels = new int[bitmap.getWidth()][bitmap.getHeight()];
visible = new boolean[bitmap.getWidth()][bitmap.getHeight()];
pixelsX = bitmap.getWidth();
pixelsY = bitmap.getHeight();
//Utility is an another class of mine, providing information about the device
drawWidth = scale * Utility.getScaleWidth();
drawHeight = scale * Utility.getScaleHeight();
width = pixelsX * drawWidth;
height = pixelsY * drawHeight;
for(int x = 0 ; x < bitmap.getWidth() ; x++){
for(int y = 0 ; y < bitmap.getHeight() ; y++){
pixels[x][y] = bitmap.getPixel(x,y);
if(Color.alpha(pixels[x][y]) == 0) visible[x][y] = false;
else visible[x][y] = true;
}
}
bitmap.recycle();
bitmap = null;
}
public void draw(float x, float y, Canvas canvas){
for(int drawY = 0 ; drawY < pixelsY ; drawY++){
for(int drawX = 0 ; drawX < pixelsX ; drawX++){
if(visible[drawX][drawY]){
boxPaint.setColor(pixels[drawX][drawY]);
canvas.drawRect(x + drawWidth * drawX, y + drawHeight * drawY,
x + drawWidth * (drawX + 1), y + drawHeight * (drawY + 1), boxPaint);
}
}
}
}
}
So, how could I make this class faster or is there a totally different way for doing this correctly?
Instead of creating a new bigger sized Bitmap you can also draw smaller sized Bitmap directly into new size using;
Canvas.drawBitmap(Bitmap, null, dstRect, Paint);
This method should re-size the original Bitmap into dstRect size and I'm quite sure there's a setting to select nearest neighbor filtering. This should enable you to draw smaller image directly into desired size whilst retaining the pixel art looks.

colourful polylines in android maps api v2

I want to draw polyline in android maps api version 2. I want it to have many colors, preferably with gradients. It seems to me though, that polylines are allowed to have only single color.
How can I do that? I already have api-v1 overlay drawing what I like, so presumably I can reuse some code
public class RouteOverlayGoogle extends Overlay {
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
//(...) draws line with color representing speed
}
I know it's been a pretty long time since this has been asked, but there are still no gradient polylines (as of writing, ~may 2015) and drawing multiple polylines really doesn't cut it (jagged edges, quite a bit of lag when dealing with several hundred of points, just not very visually appealing).
When I had to implement gradient polylines, what I ended up doing was implementing a TileOverlay that would render the polyline to a canvas and then rasterize it (see this gist for the specific code I wrote to do it https://gist.github.com/Dagothig/5f9cf0a4a7a42901a7b2).
The implementation doesn't try to do any sort of viewport culling because I ended up not needing it to reach the performance I wanted (I'm not sure about the numbers, but it was under a second per tiles, and multiple tiles will be rendered at the same time).
Rendering the gradient polyline can be pretty tricky to get properly however since you're dealing with varying viewports (positions and size): more than that, I hit a few issues with the limit on float precision at high zoom levels (20+). In the end I didn't use the scale and translate functions from the canvas because I would get weird corruption issues.
Something else to watch out for if you use a similar data structure to what I had (latitudes, longitudes and timestamps) is that you need multiple segments to render the gradient properly (I ended up working with 3 points at a time).
For posterity's sake, I'm going to also leave the code from the gist here:
(the projections are done using https://github.com/googlemaps/android-maps-utils if you're wondering where com.google.maps.android.projection.SphericalMercatorProjection comes from)
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.SphericalUtil;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import java.io.ByteArrayOutputStream;
import java.util.List;
/**
* Tile overlay used to display a colored polyline as a replacement for the non-existence of gradient
* polylines for google maps
*/
public class ColoredPolylineTileOverlay<T extends ColoredPolylineTileOverlay.PointHolder> implements TileProvider {
public static final double LOW_SPEED_CLAMP_KMpH = 0;
public static final double LOW_SPEED_CLAMP_MpS = 0;
// TODO: calculate speed as highest speed of pointsCollection
public static final double HIGH_SPEED_CLAMP_KMpH = 50;
public static final double HIGH_SPEED_CLAMP_MpS = HIGH_SPEED_CLAMP_KMpH * 1000 / (60 * 60);
public static final int BASE_TILE_SIZE = 256;
public static int[] getSpeedColors(Context context) {
return new int[] {
context.getResources().getColor(R.color.polyline_low_speed),
context.getResources().getColor(R.color.polyline_med_speed),
context.getResources().getColor(R.color.polyline_high_speed)
};
}
public static float getSpeedProportion(double metersPerSecond) {
return (float)(Math.max(Math.min(metersPerSecond, HIGH_SPEED_CLAMP_MpS), LOW_SPEED_CLAMP_MpS) / HIGH_SPEED_CLAMP_MpS);
}
public static int interpolateColor(int[] colors, float proportion) {
int rTotal = 0, gTotal = 0, bTotal = 0;
// We correct the ratio to colors.length - 1 so that
// for i == colors.length - 1 and p == 1, then the final ratio is 1 (see below)
float p = proportion * (colors.length - 1);
for (int i = 0; i < colors.length; i++) {
// The ratio mostly resides on the 1 - Math.abs(p - i) calculation :
// Since for p == i, then the ratio is 1 and for p == i + 1 or p == i -1, then the ratio is 0
// This calculation works BECAUSE p lies within [0, length - 1] and i lies within [0, length - 1] as well
float iRatio = Math.max(1 - Math.abs(p - i), 0.0f);
rTotal += (int)(Color.red(colors[i]) * iRatio);
gTotal += (int)(Color.green(colors[i]) * iRatio);
bTotal += (int)(Color.blue(colors[i]) * iRatio);
}
return Color.rgb(rTotal, gTotal, bTotal);
}
protected final Context context;
protected final PointCollection<T> pointsCollection;
protected final int[] speedColors;
protected final float density;
protected final int tileDimension;
protected final SphericalMercatorProjection projection;
// Caching calculation-related stuff
protected LatLng[] trailLatLngs;
protected Point[] projectedPts;
protected Point[] projectedPtMids;
protected double[] speeds;
public ColoredPolylineTileOverlay(Context context, PointCollection pointsCollection) {
super();
this.context = context;
this.pointsCollection = pointsCollection;
speedColors = getSpeedColors(context);
density = context.getResources().getDisplayMetrics().density;
tileDimension = (int)(BASE_TILE_SIZE * density);
projection = new SphericalMercatorProjection(BASE_TILE_SIZE);
calculatePointsAndSpeeds();
}
public void calculatePointsAndSpeeds() {
trailLatLngs = new LatLng[pointsCollection.getPoints().size()];
projectedPts = new Point[pointsCollection.getPoints().size()];
projectedPtMids = new Point[Math.max(pointsCollection.getPoints().size() - 1, 0)];
speeds = new double[Math.max(pointsCollection.getPoints().size() - 1, 0)];
List<T> points = pointsCollection.getPoints();
for (int i = 0; i < points.size(); i++) {
T point = points.get(i);
LatLng latLng = point.getLatLng();
trailLatLngs[i] = latLng;
projectedPts[i] = projection.toPoint(latLng);
// Mids
if (i > 0) {
LatLng previousLatLng = points.get(i - 1).getLatLng();
LatLng latLngMid = SphericalUtil.interpolate(previousLatLng, latLng, 0.5);
projectedPtMids[i - 1] = projection.toPoint(latLngMid);
T previousPoint = points.get(i - 1);
double speed = SphericalUtil.computeDistanceBetween(latLng, previousLatLng) / ((point.getTime() - previousPoint.getTime()) / 1000.0);
speeds[i - 1] = speed;
}
}
}
#Override
public Tile getTile(int x, int y, int zoom) {
// Because getTile can be called asynchronously by multiple threads, none of the info we keep in the class will be modified
// (getTile is essentially side-effect-less) :
// Instead, we create the bitmap, the canvas and the paints specifically for the call to getTile
Bitmap bitmap = Bitmap.createBitmap(tileDimension, tileDimension, Bitmap.Config.ARGB_8888);
// Normally, instead of the later calls for drawing being offset, we would offset them using scale() and translate() right here
// However, there seems to be funky issues related to float imprecisions that happen at large scales when using this method, so instead
// The points are offset properly when drawing
Canvas canvas = new Canvas(bitmap);
Matrix shaderMat = new Matrix();
Paint gradientPaint = new Paint();
gradientPaint.setStyle(Paint.Style.STROKE);
gradientPaint.setStrokeWidth(3f * density);
gradientPaint.setStrokeCap(Paint.Cap.BUTT);
gradientPaint.setStrokeJoin(Paint.Join.ROUND);
gradientPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
gradientPaint.setShader(new LinearGradient(0, 0, 1, 0, speedColors, null, Shader.TileMode.CLAMP));
gradientPaint.getShader().setLocalMatrix(shaderMat);
Paint colorPaint = new Paint();
colorPaint.setStyle(Paint.Style.STROKE);
colorPaint.setStrokeWidth(3f * density);
colorPaint.setStrokeCap(Paint.Cap.BUTT);
colorPaint.setStrokeJoin(Paint.Join.ROUND);
colorPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
// See https://developers.google.com/maps/documentation/android/views#zoom for handy info regarding what zoom is
float scale = (float)(Math.pow(2, zoom) * density);
renderTrail(canvas, shaderMat, gradientPaint, colorPaint, scale, x, y);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
return new Tile(tileDimension, tileDimension, baos.toByteArray());
}
public void renderTrail(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint, float scale, int x, int y) {
List<T> points = pointsCollection.getPoints();
double speed1, speed2;
MutPoint pt1 = new MutPoint(), pt2 = new MutPoint(), pt3 = new MutPoint(), pt1mid2 = new MutPoint(), pt2mid3 = new MutPoint();
// Guard statement: if the trail is only 1 point, just render the point by itself as a speed of 0
if (points.size() == 1) {
pt1.set(projectedPts[0], scale, x, y, tileDimension);
speed1 = 0;
float speedProp = getSpeedProportion(speed1);
colorPaint.setStyle(Paint.Style.FILL);
colorPaint.setColor(interpolateColor(speedColors, speedProp));
canvas.drawCircle((float) pt1.x, (float) pt1.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
colorPaint.setStyle(Paint.Style.STROKE);
return;
}
// Guard statement: if the trail is exactly 2 points long, just render a line from A to B at d(A, B) / t speed
if (points.size() == 2) {
pt1.set(projectedPts[0], scale, x, y, tileDimension);
pt2.set(projectedPts[1], scale, x, y, tileDimension);
speed1 = speeds[0];
float speedProp = getSpeedProportion(speed1);
drawLine(canvas, colorPaint, pt1, pt2, speedProp);
return;
}
// Because we want to be displaying speeds as color ratios, we need multiple points to do it properly:
// Since we use calculate the speed using the distance and the time, we need at least 2 points to calculate the distance;
// this means we know the speed for a segment, not a point.
// Furthermore, since we want to be easing the color changes between every segment, we have to use 3 points to do the easing;
// every line is split into two, and we ease over the corners
// This also means the first and last corners need to be extended to include the first and last points respectively
// Finally (you can see about that in getTile()) we need to offset the point projections based on the scale and x, y because
// weird display behaviour occurs
for (int i = 2; i < points.size(); i++) {
pt1.set(projectedPts[i - 2], scale, x, y, tileDimension);
pt2.set(projectedPts[i - 1], scale, x, y, tileDimension);
pt3.set(projectedPts[i], scale, x, y, tileDimension);
// Because we want to split the lines in two to ease over the corners, we need the middle points
pt1mid2.set(projectedPtMids[i - 2], scale, x, y, tileDimension);
pt2mid3.set(projectedPtMids[i - 1], scale, x, y, tileDimension);
// The speed is calculated in meters per second (same format as the speed clamps); because getTime() is in millis, we need to correct for that
speed1 = speeds[i - 2];
speed2 = speeds[i - 1];
float speed1Prop = getSpeedProportion(speed1);
float speed1to2Prop = getSpeedProportion((speed1 + speed2) / 2);
float speed2Prop = getSpeedProportion(speed2);
// Circle for the corner (removes the weird empty corners that occur otherwise)
colorPaint.setStyle(Paint.Style.FILL);
colorPaint.setColor(interpolateColor(speedColors, speed1to2Prop));
canvas.drawCircle((float)pt2.x, (float)pt2.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
colorPaint.setStyle(Paint.Style.STROKE);
// Corner
// Note that since for the very first point and the very last point we don't split it in two, we used them instead.
drawLine(canvas, shaderMat, gradientPaint, colorPaint, i - 2 == 0 ? pt1 : pt1mid2, pt2, speed1Prop, speed1to2Prop);
drawLine(canvas, shaderMat, gradientPaint, colorPaint, pt2, i == points.size() - 1 ? pt3 : pt2mid3, speed1to2Prop, speed2Prop);
}
}
/**
* Note: it is assumed the shader is 0, 0, 1, 0 (horizontal) so that it lines up with the rotation
* (rotations are usually setup so that the angle 0 points right)
*/
public void drawLine(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio1, float ratio2) {
// Degenerate case: both ratios are the same; we just handle it using the colorPaint (handling it using the shader is just messy and ineffective)
if (ratio1 == ratio2) {
drawLine(canvas, colorPaint, pt1, pt2, ratio1);
return;
}
shaderMat.reset();
// PS: don't ask me why this specfic orders for calls works but other orders will mess up
// Since every call is pre, this is essentially ordered as (or my understanding is that it is):
// ratio translate -> ratio scale -> scale to pt length -> translate to pt start -> rotate
// (my initial intuition was to use only post calls and to order as above, but it resulted in odd corruptions)
// Setup based on points:
// We translate the shader so that it is based on the first point, rotated towards the second and since the length of the
// gradient is 1, then scaling to the length of the distance between the points makes it exactly as long as needed
shaderMat.preRotate((float) Math.toDegrees(Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x)), (float)pt1.x, (float)pt1.y);
shaderMat.preTranslate((float)pt1.x, (float)pt1.y);
float scale = (float)Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
shaderMat.preScale(scale, scale);
// Setup based on ratio
// By basing the shader to the first ratio, we ensure that the start of the gradient corresponds to it
// The inverse scaling of the shader means that it takes the full length of the call to go to the second ratio
// For instance; if d(ratio1, ratio2) is 0.5, then the shader needs to be twice as long so that an entire call (1)
// Results in only half of the gradient being used
shaderMat.preScale(1f / (ratio2 - ratio1), 1f / (ratio2 - ratio1));
shaderMat.preTranslate(-ratio1, 0);
gradientPaint.getShader().setLocalMatrix(shaderMat);
canvas.drawLine(
(float)pt1.x,
(float)pt1.y,
(float)pt2.x,
(float)pt2.y,
gradientPaint
);
}
public void drawLine(Canvas canvas, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio) {
colorPaint.setColor(interpolateColor(speedColors, ratio));
canvas.drawLine(
(float)pt1.x,
(float)pt1.y,
(float)pt2.x,
(float)pt2.y,
colorPaint
);
}
public interface PointCollection<T extends PointHolder> {
List<T> getPoints();
}
public interface PointHolder {
LatLng getLatLng();
long getTime();
}
public static class MutPoint {
public double x, y;
public MutPoint set(Point point, float scale, int x, int y, int tileDimension) {
this.x = point.x * scale - x * tileDimension;
this.y = point.y * scale - y * tileDimension;
return this;
}
}
}
Note that this implementation assumes two relatively large things:
the polyline is already complete
that there is only one polyline.
I would assume handling (1) would not be very difficult. However, if you intend to draw multiple polylines this way, you may need to look at some ways to enhance performance (keeping a bounding box of the polylines to be able to easily discard those that do not fit the viewport for one).
One more thing to remember regarding using a TileOverlay is that it is rendered after movements are done, not during; so you may want to back up the overlay with an actual monochrome polyline underneath it to give it some continuity.
PS: this is the first time I try to answer a question, so if there's anything I should fix or do differently please tell me.
One simple solution: draw multiple polylines and individually set the color.

Sprites won't move in cocos2d for android

I am having issues with cocos2d for Android. I have tried multiple methods but can't seem to get the sprite to move across the screen. Both pictures show up, but the computer sprite does not move. There are no errors that appear while running the code, but nothing happens besides the two pictures showing up with no movement.
package com.example.minigameapp;
import java.util.Random;
import org.cocos2d.actions.instant.CCCallFuncN;
import org.cocos2d.actions.interval.CCMoveTo;
import org.cocos2d.actions.interval.CCSequence;
import org.cocos2d.layers.CCColorLayer;
import org.cocos2d.layers.CCScene;
import org.cocos2d.nodes.CCDirector;
import org.cocos2d.nodes.CCSprite;
import org.cocos2d.types.CGPoint;
import org.cocos2d.types.CGSize;
import org.cocos2d.types.ccColor4B;
import android.util.Log;
public class GameLayer extends CCColorLayer{
CCSprite _player;
CCSprite _computer;
public static CCScene scene()
{
CCScene scene = CCScene.node();
CCColorLayer layer = new GameLayer(ccColor4B.ccc4(255, 255, 255, 255));
scene.addChild(layer);
return scene;
}
protected GameLayer(ccColor4B color)
{
super(color);
this.setIsTouchEnabled(true);
CGSize winSize = CCDirector.sharedDirector().displaySize();
_player = CCSprite.sprite("race_car2.png");
_computer = CCSprite.sprite("race_car.png");
_player.setPosition(CGPoint.ccp(_player.getContentSize().width / 2.0f, winSize.height / 2.0f));
_computer.setPosition(CGPoint.ccp(_computer.getContentSize().width / 2.0f,winSize.height / 2.0f + _player.getContentSize().height*3));
addChild(_player);
addChild(_computer);
this.schedule("moveComputer",1.0f);
}
public void moveComputer(){
CGSize winSize = CCDirector.sharedDirector().displaySize();
float finalX=winSize.width;
// Determine speed of the target
Random rand = new Random();
int minDuration = 2;
int maxDuration = 15;
int rangeDuration = maxDuration - minDuration;
//int actualDuration = rand.nextInt(rangeDuration) + minDuration;
int actualDuration=1;
Log.d("GameLayer","Set Action");
//CCMoveTo actionMove = CCMoveTo.action(actualDuration, CGPoint.ccp(-computer.getContentSize().width / 2.0f, finalX));
//CCCallFuncN actionMoveDone = CCCallFuncN.action(this, "spriteMoveFinished");
//CCSequence actions = CCSequence.actions(actionMove, actionMoveDone);
//computer.runAction(actions);
CGPoint point = CGPoint.ccp(finalX,winSize.height / 2.0f + _player.getContentSize().height*3);
CCMoveTo actionMove = CCMoveTo.action(10, point);
_computer.runAction(actionMove);
Log.d("GameLayer", "Start Moving");
}
public void spriteMoveFinished(Object sender)
{
Log.d("GameLayer", "Finished Moving");
//CCSprite sprite = (CCSprite)sender;
//this.removeChild(sprite, true);
this.removeChild(_computer, true);
Log.d("GameLayer", "Remove Sprite");
}
}
You have fixed the position of CGPoint
CGPoint point = CGPoint.ccp(finalX,winSize.height / 2.0f + _player.getContentSize().height*3);
CCMoveTo actionMove = CCMoveTo.action(10, point);
You have to change the CGPoint values also in every schedule.
Your scheduled method running, but CG point value same every time method called and you have used the CCMoveTo which move the sprite to particular location on screen. So sprite is already in the postion where you want to move so that appear still(Not moved).
Change the CGPoint value every time scheduled method called...
you can check the difference by replacing CGPoint values as
CGPoint point = CGPoint.ccp(finalX+(100*Math.random()),winSize.height / 2.0f + player.getContentSize().height*3+(100*Math.random()));
One small mistake i.e, in side moveComputer() you didnt pass argumets.pass arguments like below then your object will move
public void moveComputer(float dt)
{
//
}

How to draw only a part of arc in canvas

I'm developing an analog clock widget and the dial of clock is an image.
I want to draw an arc segment like in the image(as shown in orange).
paint.setColor(Color.GREEN);
paint.setStrokeWidth(20);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(100, 100, 200, paint);
I tried with drawcircle and also with drawArc but could not proceed since i want only a part of arc and not a complete arc. Any ideas ?
I made this class hope it help you, all variable are in spanish but its quite simple,
the constructor SemiCirculo use as parameters the rgb for the semicircle and the resolution (number of triangles for your semicircle)
the CalcularPuntosPorcentaje method use as parameters the center of the circle, the starting angle, the widht of the angle, and the radio.
the method ImprimeCirculo use the canvas as parameter, it is used to draw the semicircle once it has allready been calcultaed the points of the semicircle with the previus mentioned method.
the CalcularPuntosPorcentaje method is similar to CalcularPuntosPorcentaje, but insted of the starting and the widht angle parameters it use a % from 0 to 100
finaly the SetOffset and SetColor are used to change the default starting poing o reference for the angle and the color of the semicircle
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
public class SemiCirculo {
private Path circulo;
private Paint color;
private float px, py, radio, anguloI, anchoa,offset;
private int r, g, b;
private int resolucion;
private float puntox[],puntoy[];
public SemiCirculo(int r1, int g1, int b1, int resolucion1) {
this.offset = 0;
this.color = new Paint();
this.r = r1;
this.g = g1;
this.b = b1;
this.color.setColor(Color.rgb(r, g, b));
color.setAntiAlias(true);
circulo = new Path();
this.resolucion = resolucion1;
this.puntox = new float[this.resolucion];
this.puntoy = new float[this.resolucion];
this.anguloI = 0;
this.anchoa = 1;
}
public void SetOffset(float off) {
this.offset = off;
}
public void SetColor(int r1,int g1, int b1){
this.r=r1;
this.g=g1;
this.b=b1;
this.color.setColor(Color.rgb(r, g, b));
}
public void CalcularPuntosPorcentaje(float px1, float py1,
float porcentaje, float radio1) {
this.anguloI = 0 + this.offset;
this.px = px1;
this.py = py1;
this.radio = radio1;
this.anguloI = 0;
this.anchoa = porcentaje / 100 * 360;
this.CalcularPuntos(px, py, anguloI, anchoa, radio);
}
public void CalcularPuntos(float px1, float py1, float anguloI1,
float anchoangulo, float radio1) {
this.anguloI = anguloI1 + this.offset;
this.anchoa = anchoangulo;
this.px = px1;
this.py = py1;
this.radio = radio1 ;
float angulo = 360 - this.anguloI - this.anchoa;
for (int i = 0; i < resolucion; i++) {
this.puntox[i] = this.px - (float) Math.sin(Math.toRadians(angulo))
* this.radio;
this.puntoy[i] = this.py - (float) Math.cos(Math.toRadians(angulo))
* this.radio;
angulo = (360 - this.anguloI - this.anchoa)
+ ((this.anchoa / (float) (this.resolucion)) * (i + 2));
}
this.circulo.reset();
this.circulo.moveTo(this.px, this.py);
for (int i = 0; i < resolucion; i++) {
this.circulo.lineTo(this.puntox[i], this.puntoy[i]);
}
}
public void ImprimeCirculo(Canvas canvas) {
canvas.drawPath(this.circulo, this.color);
}
}
You need to use this method:
canvas.drawArc(innerRect, -11.0f, 11.0f + 6.0f, true, paintx);
for docs see here:
http://developer.android.com/reference/android/graphics/Canvas.html#drawArc%28android.graphics.RectF,%20float,%20float,%20boolean,%20android.graphics.Paint%29
Be sure set the angle parameters correctly! And use float!
The first angle is the start of your arc and the second angle param is the sweep angle, ie how many degrees long should the angle be - measured clockwise.
Try it, it will work for sure. Just needs a little playing around :-)

Categories

Resources