/* * Copyright 2015 Daniel Dittmar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package dan.dit.whatsthat.riddle.games; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; import android.view.MotionEvent; import java.util.ArrayList; import java.util.List; import dan.dit.whatsthat.achievement.AchievementDataEvent; import dan.dit.whatsthat.achievement.AchievementProperties; import dan.dit.whatsthat.image.Image; import dan.dit.whatsthat.riddle.Riddle; import dan.dit.whatsthat.riddle.RiddleConfig; import dan.dit.whatsthat.riddle.achievement.holders.AchievementTriangle; import dan.dit.whatsthat.riddle.control.RiddleGame; import dan.dit.whatsthat.riddle.control.RiddleScore; import dan.dit.whatsthat.riddle.types.TypesHolder; import dan.dit.whatsthat.testsubject.TestSubject; import dan.dit.whatsthat.testsubject.shopping.sortiment.SortimentHolder; import dan.dit.whatsthat.util.general.PercentProgressListener; import dan.dit.whatsthat.util.compaction.CompactedDataCorruptException; import dan.dit.whatsthat.util.compaction.Compacter; import dan.dit.whatsthat.util.image.BitmapUtil; import dan.dit.whatsthat.util.listlock.ListLockMaxIndex; import dan.dit.whatsthat.util.listlock.LockDistanceRefresher; /** * Created by daniel on 09.05.15. */ public class RiddleTriangle extends RiddleGame { private static final float MIN_AREA_ESTIMATE = 30; private static final float SPLIT_MAX_DISTANCE = 40; public static final int MAX_SPLIT_PER_CLICK = 15; private static final int X_SAMPLES_COUNT = 10; private static final int Y_SAMPLES_COUNT = 10; // will only consider X_SAMPLES * Y_SAMPLES pixels per triangle private static final int MAX_TRIANGLES_FOR_BIG_SCORE_BONUS = 100; private static final int MAX_TRIANGLES_FOR_SCORE_BONUS = 500; private List<Triangle> mTriangles; private Bitmap mBackground; private Canvas mBackgroundCanvas; private Paint mClearPaint; private Paint mTrianglePaint; private ListLockMaxIndex mLock; private LockDistanceRefresher mLockRefresher; private boolean mFeatureDivideByMove; public RiddleTriangle(Riddle riddle, Image image, Bitmap bitmap, Resources res, RiddleConfig config, PercentProgressListener listener) { super(riddle, image, bitmap, res, config, listener); } @Override protected void addBonusReward(@NonNull RiddleScore.Rewardable rewardable) { int bonus = (mTriangles.size() <= MAX_TRIANGLES_FOR_BIG_SCORE_BONUS ? TypesHolder.SCORE_HARD : mTriangles.size() <= MAX_TRIANGLES_FOR_SCORE_BONUS ? TypesHolder.SCORE_SIMPLE : 0); rewardable.addBonus(bonus); } @Override public void draw(Canvas canvas) { canvas.drawBitmap(mBackground, 0, 0, null); } @Override public void onClose() { super.onClose(); mTriangles = null; mBackground = null; mBackgroundCanvas = null; mTrianglePaint = null; mClearPaint = null; } @Override protected void initBitmap(Resources res, PercentProgressListener listener) { mBackground = Bitmap.createBitmap(mConfig.mWidth, mConfig.mHeight, mBitmap.getConfig()); listener.onProgressUpdate(25); mBackgroundCanvas = new Canvas(mBackground); mClearPaint = new Paint(); mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); mFeatureDivideByMove = TestSubject.getInstance().hasFeature(SortimentHolder.ARTICLE_KEY_TRIANGLE_DIVIDE_BY_MOVE_FEATURE); mTriangles = new ArrayList<>(); mLock = new ListLockMaxIndex(mTriangles, ListLockMaxIndex.UNLIMITED_ELEMENTS); mLockRefresher = new LockDistanceRefresher(mLock, Math.min(mConfig.mWidth, mConfig.mHeight) / 2.f); mTrianglePaint = new Paint(); mTrianglePaint.setAntiAlias(true); mTrianglePaint.setStyle(Paint.Style.FILL_AND_STROKE); mTrianglePaint.setStrokeWidth(0.35f * mConfig.mScreenDensity / DisplayMetrics.DENSITY_HIGH); mTrianglePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); listener.onProgressUpdate(50); initTriangles(getCurrentState()); } private void initTriangles(Compacter cmp) { boolean error = false; if (cmp != null && cmp.getSize() > 2) { int loadedWidth = 0; int loadedHeight = 0; try { loadedWidth = cmp.getInt(0); loadedHeight = cmp.getInt(1); } catch (CompactedDataCorruptException e) { error = true; } if (!error && loadedWidth == mConfig.mWidth && loadedHeight == mConfig.mHeight) { for (int i = 3; i + 5 < cmp.getSize(); i += 6) { try { Triangle t = new Triangle(cmp.getFloat(i), cmp.getFloat(i + 1), cmp.getFloat(i + 2), cmp.getFloat(i + 3), cmp.getFloat(i + 4), cmp.getFloat(i + 5)); t.draw(mBitmap, mBackgroundCanvas, mTrianglePaint); mTriangles.add(t); } catch (CompactedDataCorruptException e) { Log.e("Riddle", "Compacted data corrupt for triangle: " + e); error = true; break; } } } } if (cmp == null || error) { mTriangles.clear(); mTriangles.add(new Triangle(0f, 0f, 0f, mConfig.mHeight, mConfig.mWidth, mConfig.mHeight).draw(mBitmap, mBackgroundCanvas, mTrianglePaint)); mTriangles.add(new Triangle(0f, 0f, mConfig.mWidth, 0, mConfig.mWidth, mConfig.mHeight).draw(mBitmap, mBackgroundCanvas, mTrianglePaint)); } } private int onClick(float x, float y, int maxCount) { int stopBefore = mTriangles.size(); int splitCount = 0; for (int i = 0; i < stopBefore && mLock.isUnlocked(i) && splitCount < maxCount; i++) { Triangle curr = mTriangles.get(i); if (curr.areaEstimate() > MIN_AREA_ESTIMATE && (curr.isInside(x, y) || curr.minCornerDistSquared(x, y) < SPLIT_MAX_DISTANCE * SPLIT_MAX_DISTANCE)) { mTriangles.remove(curr); int delta = -1 + curr.splitAndAdd(mTriangles, mBitmap, mBackgroundCanvas, mClearPaint, mTrianglePaint); splitCount++; i -= delta; stopBefore -= delta; mLock.lock(delta); } } return splitCount; } @Override public boolean onMotionEvent(MotionEvent event) { mLockRefresher.update(event); if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { int splitCount = onClick(event.getX(), event.getY(), MAX_SPLIT_PER_CLICK); if (splitCount > 0) { updateAchievementTriangleCount(AchievementTriangle.KEY_TRIANGLE_DIVIDED_BY_CLICK, splitCount); } return splitCount > 0; } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE && mFeatureDivideByMove) { int splitCount = onClick(event.getX(), event.getY(), (int) Math.max(1, MAX_SPLIT_PER_CLICK / 3.)); if (splitCount > 0) { updateAchievementTriangleCount(AchievementTriangle.KEY_TRIANGLE_DIVIDED_BY_MOVE, splitCount); } return splitCount > 0; } return false; } private void updateAchievementTriangleCount(String divisionTypeKey, int delta) { mConfig.mAchievementGameData.enableSilentChanges(AchievementDataEvent.EVENT_TYPE_DATA_UPDATE); if (!TextUtils.isEmpty(divisionTypeKey) && delta > 0) { mConfig.mAchievementGameData.increment(divisionTypeKey, (long) delta, 0L); mConfig.mAchievementGameData.putValue(AchievementTriangle.KEY_TRIANGLE_COUNT_BEFORE_LAST_INTERACTION, (long) (mTriangles.size() - delta), AchievementProperties.UPDATE_POLICY_ALWAYS); } mConfig.mAchievementGameData.putValue(AchievementTriangle.KEY_TRIANGLE_COUNT, (long) mTriangles.size(), AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.disableSilentChanges(); } @Override public Bitmap makeSnapshot() { int width = SNAPSHOT_DIMENSION.getWidthForDensity(mConfig.mScreenDensity); int height = SNAPSHOT_DIMENSION.getHeightForDensity(mConfig.mScreenDensity); return BitmapUtil.resize(mBackground, width, height); } @Override protected void initAchievementData() { updateAchievementTriangleCount(null, 0); } @NonNull @Override protected String compactCurrentState() { Compacter cmp = new Compacter(); cmp.appendData(mConfig.mWidth); cmp.appendData(mConfig.mHeight); cmp.appendData(""); // in case we need it for (Triangle t : mTriangles) { cmp.appendData(t.x1); cmp.appendData(t.y1); cmp.appendData(t.x2); cmp.appendData(t.y2); cmp.appendData(t.x3); cmp.appendData(t.y3); } return cmp.compact(); } private static float distSquared(float x1, float y1, float x2, float y2) { return (x1-x2) * (x1-x2) + (y1-y2) * (y1-y2); } private static class Triangle { float x1, y1, x2, y2, x3, y3; private static final RectF BOUND = new RectF(); private static final Path LINES = new Path(); private Triangle(float x1, float y1, float x2, float y2, float x3, float y3) { this.x1 = x1; this.y1= y1; this.x2 = x2; this.y2 = y2; this.x3 = x3; this.y3 = y3; } private float areaEstimate() { // exact for our triangles, but not for general triangles return 0.5f * Math.max(Math.abs(x1 - x2), Math.abs(x1 - x3)) * Math.max(Math.abs(y1 - y2), Math.abs(y1 - y3)); } private Triangle draw(Bitmap bitmap, Canvas canvas, Paint paint) { initPath(); int rgb = calculateColor(bitmap); paint.setColor(rgb); canvas.drawPath(LINES, paint); return this; } private int splitAndAdd(List<Triangle> triangles, Bitmap bitmap, Canvas canvas, Paint clear, Paint paint) { initPath(); //canvas.drawPath(LINES, clear); float d1 = distSquared(x1, y1, x2, y2); float d2 = distSquared(x1, y1, x3, y3); float d3 = distSquared(x2, y2, x3, y3); float p1x, p1y, p2x, p2y, p3x, p3y, sharedx, sharedy; if (d1 > d2 && d1 > d3) { p1x = x1; p1y = y1; p2x = x2; p2y = y2; p3x = x3; p3y = y3; } else if (d2 > d1 && d2 > d3) { p1x = x1; p1y = y1; p2x = x3; p2y = y3; p3x = x2; p3y = y2; } else { p1x = x2; p1y = y2; p2x = x3; p2y = y3; p3x = x1; p3y = y1; } sharedx = p1x + (p2x - p1x) * 0.5f; sharedy = p1y + (p2y - p1y) * 0.5f; Triangle t1 = new Triangle(p1x, p1y, sharedx, sharedy, p3x, p3y); Triangle t2 = new Triangle(p2x, p2y, sharedx, sharedy, p3x, p3y); t1.draw(bitmap, canvas, paint); t2.draw(bitmap, canvas, paint); triangles.add(t1); triangles.add(t2); return 2; } private void initPath() { LINES.rewind(); LINES.moveTo(x1, y1); LINES.lineTo(x2, y2); LINES.lineTo(x3, y3); LINES.close(); } private int calculateColor(Bitmap forColor) { LINES.computeBounds(BOUND, true); int red = 0; int green = 0; int blue = 0; int alpha = 0; int pixelInTriangle = 0; final int xStepSize = Math.max(1, (int) (BOUND.right - BOUND.left) / X_SAMPLES_COUNT); final int yStepSize = Math.max(1, (int) (BOUND.bottom - BOUND.top) / Y_SAMPLES_COUNT); for (int x = (int) BOUND.left; x < BOUND.right; x+=xStepSize) { for (int y = (int) BOUND.top; y < BOUND.bottom; y+=yStepSize) { if (x < forColor.getWidth() && y < forColor.getHeight() && isInside(x, y)) { int bitmapRgb = forColor.getPixel(x, y); red += Color.red(bitmapRgb); green += Color.green(bitmapRgb); blue += Color.blue(bitmapRgb); alpha += Color.alpha(bitmapRgb); pixelInTriangle++; } } } if (pixelInTriangle > 0) { red /= pixelInTriangle; green /= pixelInTriangle; blue /= pixelInTriangle; alpha /= pixelInTriangle; return Color.argb(alpha, red, green, blue); } else { return Color.TRANSPARENT; } } private static float sign(float x1, float y1, float x2, float y2, float x3, float y3) { return (x1 - x3) * (y2 - y3) - (x2 - x3) * (y1 - y3); } private boolean isInside (float x, float y) { boolean b1, b2, b3; b1 = sign(x, y, x1, y1, x2, y2) < 0.0f; b2 = sign(x, y, x2, y2, x3, y3) < 0.0f; b3 = sign(x, y, x3, y3, x1, y1) < 0.0f; return ((b1 == b2) && (b2 == b3)); } public float minCornerDistSquared(float x, float y) { float d1 = distSquared(x1, y1, x, y); float d2 = distSquared(x2, y2, x, y); float d3 = distSquared(x3, y3, x, y); return Math.min(d1, Math.min(d2, d3)); } } }