diff options
Diffstat (limited to 'src/com/android/gallery3d/filtershow/imageshow')
20 files changed, 4810 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java b/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java new file mode 100644 index 000000000..aaec728a6 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +public class ControlPoint implements Comparable { + public float x; + public float y; + + public ControlPoint(float px, float py) { + x = px; + y = py; + } + + public ControlPoint(ControlPoint point) { + x = point.x; + y = point.y; + } + + public boolean sameValues(ControlPoint other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + + if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x)) { + return false; + } + if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y)) { + return false; + } + return true; + } + + public ControlPoint copy() { + return new ControlPoint(x, y); + } + + @Override + public int compareTo(Object another) { + ControlPoint p = (ControlPoint) another; + if (p.x < x) { + return 1; + } else if (p.x > x) { + return -1; + } + return 0; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java new file mode 100644 index 000000000..8ceb37599 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.RectF; +import android.graphics.Shader; + +import com.android.gallery3d.R; + +public class EclipseControl { + private float mCenterX = Float.NaN; + private float mCenterY = 0; + private float mRadiusX = 200; + private float mRadiusY = 300; + private static int MIN_TOUCH_DIST = 80;// should be a resource & in dips + + private float[] handlex = new float[9]; + private float[] handley = new float[9]; + private int mSliderColor; + private int mCenterDotSize = 40; + private float mDownX; + private float mDownY; + private float mDownCenterX; + private float mDownCenterY; + private float mDownRadiusX; + private float mDownRadiusY; + private Matrix mScrToImg; + + private boolean mShowReshapeHandles = true; + public final static int HAN_CENTER = 0; + public final static int HAN_NORTH = 7; + public final static int HAN_NE = 8; + public final static int HAN_EAST = 1; + public final static int HAN_SE = 2; + public final static int HAN_SOUTH = 3; + public final static int HAN_SW = 4; + public final static int HAN_WEST = 5; + public final static int HAN_NW = 6; + + public EclipseControl(Context context) { + mSliderColor = Color.WHITE; + } + + public void setRadius(float x, float y) { + mRadiusX = x; + mRadiusY = y; + } + + public void setCenter(float x, float y) { + mCenterX = x; + mCenterY = y; + } + + public int getCloseHandle(float x, float y) { + float min = Float.MAX_VALUE; + int handle = -1; + for (int i = 0; i < handlex.length; i++) { + float dx = handlex[i] - x; + float dy = handley[i] - y; + float dist = dx * dx + dy * dy; + if (dist < min) { + min = dist; + handle = i; + } + } + + if (min < MIN_TOUCH_DIST * MIN_TOUCH_DIST) { + return handle; + } + for (int i = 0; i < handlex.length; i++) { + float dx = handlex[i] - x; + float dy = handley[i] - y; + float dist = (float) Math.sqrt(dx * dx + dy * dy); + } + + return -1; + } + + public void setScrToImageMatrix(Matrix scrToImg) { + mScrToImg = scrToImg; + } + + public void actionDown(float x, float y, Oval oval) { + float[] point = new float[] { + x, y }; + mScrToImg.mapPoints(point); + mDownX = point[0]; + mDownY = point[1]; + mDownCenterX = oval.getCenterX(); + mDownCenterY = oval.getCenterY(); + mDownRadiusX = oval.getRadiusX(); + mDownRadiusY = oval.getRadiusY(); + } + + public void actionMove(int handle, float x, float y, Oval oval) { + float[] point = new float[] { + x, y }; + mScrToImg.mapPoints(point); + x = point[0]; + y = point[1]; + + // Test if the matrix is swapping x and y + point[0] = 0; + point[1] = 1; + mScrToImg.mapVectors(point); + boolean swapxy = (point[0] > 0.0f); + + int sign = 1; + switch (handle) { + case HAN_CENTER: + float ctrdx = mDownX - mDownCenterX; + float ctrdy = mDownY - mDownCenterY; + oval.setCenter(x - ctrdx, y - ctrdy); + // setRepresentation(mVignetteRep); + break; + case HAN_NORTH: + sign = -1; + case HAN_SOUTH: + if (swapxy) { + float raddx = mDownRadiusY - Math.abs(mDownX - mDownCenterY); + oval.setRadiusY(Math.abs(x - oval.getCenterY() + sign * raddx)); + } else { + float raddy = mDownRadiusY - Math.abs(mDownY - mDownCenterY); + oval.setRadiusY(Math.abs(y - oval.getCenterY() + sign * raddy)); + } + break; + case HAN_EAST: + sign = -1; + case HAN_WEST: + if (swapxy) { + float raddy = mDownRadiusX - Math.abs(mDownY - mDownCenterX); + oval.setRadiusX(Math.abs(y - oval.getCenterX() + sign * raddy)); + } else { + float raddx = mDownRadiusX - Math.abs(mDownX - mDownCenterX); + oval.setRadiusX(Math.abs(x - oval.getCenterX() - sign * raddx)); + } + break; + case HAN_SE: + case HAN_NE: + case HAN_SW: + case HAN_NW: + float sin45 = (float) Math.sin(45); + float dr = (mDownRadiusX + mDownRadiusY) * sin45; + float ctr_dx = mDownX - mDownCenterX; + float ctr_dy = mDownY - mDownCenterY; + float downRad = Math.abs(ctr_dx) + Math.abs(ctr_dy) - dr; + float rx = oval.getRadiusX(); + float ry = oval.getRadiusY(); + float r = (Math.abs(rx) + Math.abs(ry)) * sin45; + float dx = x - oval.getCenterX(); + float dy = y - oval.getCenterY(); + float nr = Math.abs(Math.abs(dx) + Math.abs(dy) - downRad); + oval.setRadius(rx * nr / r, ry * nr / r); + + break; + } + } + + public void paintGrayPoint(Canvas canvas, float x, float y) { + if (x == Float.NaN) { + return; + } + + Paint paint = new Paint(); + + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.BLUE); + int[] colors3 = new int[] { + Color.GRAY, Color.LTGRAY, 0x66000000, 0 }; + RadialGradient g = new RadialGradient(x, y, mCenterDotSize, colors3, new float[] { + 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP); + paint.setShader(g); + canvas.drawCircle(x, y, mCenterDotSize, paint); + } + + public void paintPoint(Canvas canvas, float x, float y) { + if (x == Float.NaN) { + return; + } + + Paint paint = new Paint(); + + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.BLUE); + int[] colors3 = new int[] { + mSliderColor, mSliderColor, 0x66000000, 0 }; + RadialGradient g = new RadialGradient(x, y, mCenterDotSize, colors3, new float[] { + 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP); + paint.setShader(g); + canvas.drawCircle(x, y, mCenterDotSize, paint); + } + + void paintRadius(Canvas canvas, float cx, float cy, float rx, float ry) { + if (cx == Float.NaN) { + return; + } + int mSliderColor = 0xFF33B5E5; + Paint paint = new Paint(); + RectF rect = new RectF(cx - rx, cy - ry, cx + rx, cy + ry); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(6); + paint.setColor(Color.BLACK); + paintOvallines(canvas, rect, paint, cx, cy, rx, ry); + + paint.setStrokeWidth(3); + paint.setColor(Color.WHITE); + paintOvallines(canvas, rect, paint, cx, cy, rx, ry); + } + + public void paintOvallines( + Canvas canvas, RectF rect, Paint paint, float cx, float cy, float rx, float ry) { + canvas.drawOval(rect, paint); + float da = 4; + float arclen = da + da; + if (mShowReshapeHandles) { + paint.setStyle(Paint.Style.STROKE); + + for (int i = 0; i < 361; i += 90) { + float dx = rx + 10; + float dy = ry + 10; + rect.left = cx - dx; + rect.top = cy - dy; + rect.right = cx + dx; + rect.bottom = cy + dy; + canvas.drawArc(rect, i - da, arclen, false, paint); + dx = rx - 10; + dy = ry - 10; + rect.left = cx - dx; + rect.top = cy - dy; + rect.right = cx + dx; + rect.bottom = cy + dy; + canvas.drawArc(rect, i - da, arclen, false, paint); + } + } + da *= 2; + paint.setStyle(Paint.Style.FILL); + + for (int i = 45; i < 361; i += 90) { + double angle = Math.PI * i / 180.; + float x = cx + (float) (rx * Math.cos(angle)); + float y = cy + (float) (ry * Math.sin(angle)); + canvas.drawRect(x - da, y - da, x + da, y + da, paint); + } + paint.setStyle(Paint.Style.STROKE); + rect.left = cx - rx; + rect.top = cy - ry; + rect.right = cx + rx; + rect.bottom = cy + ry; + } + + public void fillHandles(Canvas canvas, float cx, float cy, float rx, float ry) { + handlex[0] = cx; + handley[0] = cy; + int k = 1; + + for (int i = 0; i < 360; i += 45) { + double angle = Math.PI * i / 180.; + + float x = cx + (float) (rx * Math.cos(angle)); + float y = cy + (float) (ry * Math.sin(angle)); + handlex[k] = x; + handley[k] = y; + + k++; + } + } + + public void draw(Canvas canvas) { + paintRadius(canvas, mCenterX, mCenterY, mRadiusX, mRadiusY); + fillHandles(canvas, mCenterX, mCenterY, mRadiusX, mRadiusY); + paintPoint(canvas, mCenterX, mCenterY); + } + + public boolean isUndefined() { + return Float.isNaN(mCenterX); + } + + public void setShowReshapeHandles(boolean showReshapeHandles) { + this.mShowReshapeHandles = showReshapeHandles; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java new file mode 100644 index 000000000..81394f142 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; + +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; +import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation; +import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation.Mirror; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation.Rotation; +import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +import java.util.Collection; +import java.util.Iterator; + +public final class GeometryMathUtils { + private GeometryMathUtils() {}; + + // Holder class for Geometry data. + public static final class GeometryHolder { + public Rotation rotation = FilterRotateRepresentation.getNil(); + public float straighten = FilterStraightenRepresentation.getNil(); + public RectF crop = FilterCropRepresentation.getNil(); + public Mirror mirror = FilterMirrorRepresentation.getNil(); + + public void set(GeometryHolder h) { + rotation = h.rotation; + straighten = h.straighten; + crop.set(h.crop); + mirror = h.mirror; + } + + public void wipe() { + rotation = FilterRotateRepresentation.getNil(); + straighten = FilterStraightenRepresentation.getNil(); + crop = FilterCropRepresentation.getNil(); + mirror = FilterMirrorRepresentation.getNil(); + } + + public boolean isNil() { + return rotation == FilterRotateRepresentation.getNil() && + straighten == FilterStraightenRepresentation.getNil() && + crop.equals(FilterCropRepresentation.getNil()) && + mirror == FilterMirrorRepresentation.getNil(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GeometryHolder)) { + return false; + } + GeometryHolder h = (GeometryHolder) o; + return rotation == h.rotation && straighten == h.straighten && + ((crop == null && h.crop == null) || (crop != null && crop.equals(h.crop))) && + mirror == h.mirror; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + "rotation:" + rotation.value() + + ",straighten:" + straighten + ",crop:" + crop.toString() + + ",mirror:" + mirror.value() + "]"; + } + } + + // Math operations for 2d vectors + public static float clamp(float i, float low, float high) { + return Math.max(Math.min(i, high), low); + } + + public static float[] lineIntersect(float[] line1, float[] line2) { + float a0 = line1[0]; + float a1 = line1[1]; + float b0 = line1[2]; + float b1 = line1[3]; + float c0 = line2[0]; + float c1 = line2[1]; + float d0 = line2[2]; + float d1 = line2[3]; + float t0 = a0 - b0; + float t1 = a1 - b1; + float t2 = b0 - d0; + float t3 = d1 - b1; + float t4 = c0 - d0; + float t5 = c1 - d1; + + float denom = t1 * t4 - t0 * t5; + if (denom == 0) + return null; + float u = (t3 * t4 + t5 * t2) / denom; + float[] intersect = { + b0 + u * t0, b1 + u * t1 + }; + return intersect; + } + + public static float[] shortestVectorFromPointToLine(float[] point, float[] line) { + float x1 = line[0]; + float x2 = line[2]; + float y1 = line[1]; + float y2 = line[3]; + float xdelt = x2 - x1; + float ydelt = y2 - y1; + if (xdelt == 0 && ydelt == 0) + return null; + float u = ((point[0] - x1) * xdelt + (point[1] - y1) * ydelt) + / (xdelt * xdelt + ydelt * ydelt); + float[] ret = { + (x1 + u * (x2 - x1)), (y1 + u * (y2 - y1)) + }; + float[] vec = { + ret[0] - point[0], ret[1] - point[1] + }; + return vec; + } + + // A . B + public static float dotProduct(float[] a, float[] b) { + return a[0] * b[0] + a[1] * b[1]; + } + + public static float[] normalize(float[] a) { + float length = (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]); + float[] b = { + a[0] / length, a[1] / length + }; + return b; + } + + // A onto B + public static float scalarProjection(float[] a, float[] b) { + float length = (float) Math.sqrt(b[0] * b[0] + b[1] * b[1]); + return dotProduct(a, b) / length; + } + + public static float[] getVectorFromPoints(float[] point1, float[] point2) { + float[] p = { + point2[0] - point1[0], point2[1] - point1[1] + }; + return p; + } + + public static float[] getUnitVectorFromPoints(float[] point1, float[] point2) { + float[] p = { + point2[0] - point1[0], point2[1] - point1[1] + }; + float length = (float) Math.sqrt(p[0] * p[0] + p[1] * p[1]); + p[0] = p[0] / length; + p[1] = p[1] / length; + return p; + } + + public static void scaleRect(RectF r, float scale) { + r.set(r.left * scale, r.top * scale, r.right * scale, r.bottom * scale); + } + + // A - B + public static float[] vectorSubtract(float[] a, float[] b) { + int len = a.length; + if (len != b.length) + return null; + float[] ret = new float[len]; + for (int i = 0; i < len; i++) { + ret[i] = a[i] - b[i]; + } + return ret; + } + + public static float vectorLength(float[] a) { + return (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]); + } + + public static float scale(float oldWidth, float oldHeight, float newWidth, float newHeight) { + if (oldHeight == 0 || oldWidth == 0 || (oldWidth == newWidth && oldHeight == newHeight)) { + return 1; + } + return Math.min(newWidth / oldWidth, newHeight / oldHeight); + } + + public static Rect roundNearest(RectF r) { + Rect q = new Rect(Math.round(r.left), Math.round(r.top), Math.round(r.right), + Math.round(r.bottom)); + return q; + } + + private static void concatMirrorMatrix(Matrix m, Mirror type) { + if (type == Mirror.HORIZONTAL) { + m.postScale(-1, 1); + } else if (type == Mirror.VERTICAL) { + m.postScale(1, -1); + } else if (type == Mirror.BOTH) { + m.postScale(1, -1); + m.postScale(-1, 1); + } + } + + private static int getRotationForOrientation(int orientation) { + switch (orientation) { + case ImageLoader.ORI_ROTATE_90: + return 90; + case ImageLoader.ORI_ROTATE_180: + return 180; + case ImageLoader.ORI_ROTATE_270: + return 270; + default: + return 0; + } + } + + public static GeometryHolder unpackGeometry(Collection<FilterRepresentation> geometry) { + GeometryHolder holder = new GeometryHolder(); + unpackGeometry(holder, geometry); + return holder; + } + + public static void unpackGeometry(GeometryHolder out, + Collection<FilterRepresentation> geometry) { + out.wipe(); + // Get geometry data from filters + for (FilterRepresentation r : geometry) { + if (r.isNil()) { + continue; + } + if (r.getSerializationName() == FilterRotateRepresentation.SERIALIZATION_NAME) { + out.rotation = ((FilterRotateRepresentation) r).getRotation(); + } else if (r.getSerializationName() == + FilterStraightenRepresentation.SERIALIZATION_NAME) { + out.straighten = ((FilterStraightenRepresentation) r).getStraighten(); + } else if (r.getSerializationName() == FilterCropRepresentation.SERIALIZATION_NAME) { + ((FilterCropRepresentation) r).getCrop(out.crop); + } else if (r.getSerializationName() == FilterMirrorRepresentation.SERIALIZATION_NAME) { + out.mirror = ((FilterMirrorRepresentation) r).getMirror(); + } + } + } + + public static void replaceInstances(Collection<FilterRepresentation> geometry, + FilterRepresentation rep) { + Iterator<FilterRepresentation> iter = geometry.iterator(); + while (iter.hasNext()) { + FilterRepresentation r = iter.next(); + if (ImagePreset.sameSerializationName(rep, r)) { + iter.remove(); + } + } + if (!rep.isNil()) { + geometry.add(rep); + } + } + + public static void initializeHolder(GeometryHolder outHolder, + FilterRepresentation currentLocal) { + Collection<FilterRepresentation> geometry = MasterImage.getImage().getPreset() + .getGeometryFilters(); + replaceInstances(geometry, currentLocal); + unpackGeometry(outHolder, geometry); + } + + private static Bitmap applyFullGeometryMatrix(Bitmap image, GeometryHolder holder) { + int width = image.getWidth(); + int height = image.getHeight(); + RectF crop = getTrueCropRect(holder, width, height); + Rect frame = new Rect(); + crop.roundOut(frame); + Matrix m = getCropSelectionToScreenMatrix(null, holder, width, height, frame.width(), + frame.height()); + Bitmap temp = Bitmap.createBitmap(frame.width(), frame.height(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(temp); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setFilterBitmap(true); + paint.setDither(true); + canvas.drawBitmap(image, m, paint); + return temp; + } + + public static Matrix getImageToScreenMatrix(Collection<FilterRepresentation> geometry, + boolean reflectRotation, Rect bmapDimens, float viewWidth, float viewHeight) { + GeometryHolder h = unpackGeometry(geometry); + return GeometryMathUtils.getOriginalToScreen(h, reflectRotation, bmapDimens.width(), + bmapDimens.height(), viewWidth, viewHeight); + } + + public static Matrix getOriginalToScreen(GeometryHolder holder, boolean rotate, + float originalWidth, + float originalHeight, float viewWidth, float viewHeight) { + int orientation = MasterImage.getImage().getZoomOrientation(); + int rotation = getRotationForOrientation(orientation); + Rotation prev = holder.rotation; + rotation = (rotation + prev.value()) % 360; + holder.rotation = Rotation.fromValue(rotation); + Matrix m = getCropSelectionToScreenMatrix(null, holder, (int) originalWidth, + (int) originalHeight, (int) viewWidth, (int) viewHeight); + holder.rotation = prev; + return m; + } + + public static Bitmap applyGeometryRepresentations(Collection<FilterRepresentation> res, + Bitmap image) { + GeometryHolder holder = unpackGeometry(res); + Bitmap bmap = image; + // If there are geometry changes, apply them to the image + if (!holder.isNil()) { + bmap = applyFullGeometryMatrix(bmap, holder); + } + return bmap; + } + + public static RectF drawTransformedCropped(GeometryHolder holder, Canvas canvas, + Bitmap photo, int viewWidth, int viewHeight) { + if (photo == null) { + return null; + } + RectF crop = new RectF(); + Matrix m = getCropSelectionToScreenMatrix(crop, holder, photo.getWidth(), photo.getHeight(), + viewWidth, viewHeight); + canvas.save(); + canvas.clipRect(crop); + Paint p = new Paint(); + p.setAntiAlias(true); + canvas.drawBitmap(photo, m, p); + canvas.restore(); + return crop; + } + + public static boolean needsDimensionSwap(Rotation rotation) { + switch (rotation) { + case NINETY: + case TWO_SEVENTY: + return true; + default: + return false; + } + } + + // Gives matrix for rotated, straightened, mirrored bitmap centered at 0,0. + private static Matrix getFullGeometryMatrix(GeometryHolder holder, int bitmapWidth, + int bitmapHeight) { + float centerX = bitmapWidth / 2f; + float centerY = bitmapHeight / 2f; + Matrix m = new Matrix(); + m.setTranslate(-centerX, -centerY); + m.postRotate(holder.straighten + holder.rotation.value()); + concatMirrorMatrix(m, holder.mirror); + return m; + } + + public static Matrix getFullGeometryToScreenMatrix(GeometryHolder holder, int bitmapWidth, + int bitmapHeight, int viewWidth, int viewHeight) { + float scale = GeometryMathUtils.scale(bitmapWidth, bitmapHeight, viewWidth, viewHeight); + Matrix m = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight); + m.postScale(scale, scale); + m.postTranslate(viewWidth / 2f, viewHeight / 2f); + return m; + } + + public static RectF getTrueCropRect(GeometryHolder holder, int bitmapWidth, int bitmapHeight) { + RectF r = new RectF(holder.crop); + FilterCropRepresentation.findScaledCrop(r, bitmapWidth, bitmapHeight); + float s = holder.straighten; + holder.straighten = 0; + Matrix m1 = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight); + holder.straighten = s; + m1.mapRect(r); + return r; + } + + public static Matrix getCropSelectionToScreenMatrix(RectF outCrop, GeometryHolder holder, + int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) { + Matrix m = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight); + RectF crop = getTrueCropRect(holder, bitmapWidth, bitmapHeight); + float scale = GeometryMathUtils.scale(crop.width(), crop.height(), viewWidth, viewHeight); + m.postScale(scale, scale); + GeometryMathUtils.scaleRect(crop, scale); + m.postTranslate(viewWidth / 2f - crop.centerX(), viewHeight / 2f - crop.centerY()); + if (outCrop != null) { + crop.offset(viewWidth / 2f - crop.centerX(), viewHeight / 2f - crop.centerY()); + outCrop.set(crop); + } + return m; + } + + public static Matrix getCropSelectionToScreenMatrix(RectF outCrop, + Collection<FilterRepresentation> res, int bitmapWidth, int bitmapHeight, int viewWidth, + int viewHeight) { + GeometryHolder holder = unpackGeometry(res); + return getCropSelectionToScreenMatrix(outCrop, holder, bitmapWidth, bitmapHeight, + viewWidth, viewHeight); + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/GradControl.java b/src/com/android/gallery3d/filtershow/imageshow/GradControl.java new file mode 100644 index 000000000..964da99e9 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/GradControl.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.Shader; + +import com.android.gallery3d.R; + +public class GradControl { + private float mPoint1X = Float.NaN; // used to flag parameters have not been set + private float mPoint1Y = 0; + private float mPoint2X = 200; + private float mPoint2Y = 300; + private int mMinTouchDist = 80;// should be a resource & in dips + + private float[] handlex = new float[3]; + private float[] handley = new float[3]; + private int mSliderColor; + private int mCenterDotSize; + private float mDownX; + private float mDownY; + private float mDownPoint1X; + private float mDownPoint1Y; + private float mDownPoint2X; + private float mDownPoint2Y; + Rect mImageBounds; + int mImageHeight; + private Matrix mScrToImg; + Paint mPaint = new Paint(); + DashPathEffect mDash = new DashPathEffect(new float[]{30, 30}, 0); + private boolean mShowReshapeHandles = true; + public final static int HAN_CENTER = 0; + public final static int HAN_NORTH = 2; + public final static int HAN_SOUTH = 1; + private int[] mPointColorPatern; + private int[] mGrayPointColorPatern; + private float[] mPointRadialPos = new float[]{0, .3f, .31f, 1}; + private int mLineColor; + private int mlineShadowColor; + + public GradControl(Context context) { + + Resources res = context.getResources(); + mCenterDotSize = (int) res.getDimension(R.dimen.gradcontrol_dot_size); + mMinTouchDist = (int) res.getDimension(R.dimen.gradcontrol_min_touch_dist); + int grayPointCenterColor = res.getColor(R.color.gradcontrol_graypoint_center); + int grayPointEdgeColor = res.getColor(R.color.gradcontrol_graypoint_edge); + int pointCenterColor = res.getColor(R.color.gradcontrol_point_center); + int pointEdgeColor = res.getColor(R.color.gradcontrol_point_edge); + int pointShadowStartColor = res.getColor(R.color.gradcontrol_point_shadow_start); + int pointShadowEndColor = res.getColor(R.color.gradcontrol_point_shadow_end); + mPointColorPatern = new int[]{ + pointCenterColor, pointEdgeColor, pointShadowStartColor, pointShadowEndColor}; + mGrayPointColorPatern = new int[]{ + grayPointCenterColor, grayPointEdgeColor, pointShadowStartColor, pointShadowEndColor}; + mSliderColor = Color.WHITE; + mLineColor = res.getColor(R.color.gradcontrol_line_color); + mlineShadowColor = res.getColor(R.color.gradcontrol_line_shadow); + } + + public void setPoint2(float x, float y) { + mPoint2X = x; + mPoint2Y = y; + } + + public void setPoint1(float x, float y) { + mPoint1X = x; + mPoint1Y = y; + } + + public int getCloseHandle(float x, float y) { + float min = Float.MAX_VALUE; + int handle = -1; + for (int i = 0; i < handlex.length; i++) { + float dx = handlex[i] - x; + float dy = handley[i] - y; + float dist = dx * dx + dy * dy; + if (dist < min) { + min = dist; + handle = i; + } + } + + if (min < mMinTouchDist * mMinTouchDist) { + return handle; + } + for (int i = 0; i < handlex.length; i++) { + float dx = handlex[i] - x; + float dy = handley[i] - y; + float dist = (float) Math.sqrt(dx * dx + dy * dy); + } + + return -1; + } + + public void setScrImageInfo(Matrix scrToImg, Rect imageBounds) { + mScrToImg = scrToImg; + mImageBounds = new Rect(imageBounds); + } + + private boolean centerIsOutside(float x1, float y1, float x2, float y2) { + return (!mImageBounds.contains((int) ((x1 + x2) / 2), (int) ((y1 + y2) / 2))); + } + + public void actionDown(float x, float y, Line line) { + float[] point = new float[]{ + x, y}; + mScrToImg.mapPoints(point); + mDownX = point[0]; + mDownY = point[1]; + mDownPoint1X = line.getPoint1X(); + mDownPoint1Y = line.getPoint1Y(); + mDownPoint2X = line.getPoint2X(); + mDownPoint2Y = line.getPoint2Y(); + } + + public void actionMove(int handle, float x, float y, Line line) { + float[] point = new float[]{ + x, y}; + mScrToImg.mapPoints(point); + x = point[0]; + y = point[1]; + + // Test if the matrix is swapping x and y + point[0] = 0; + point[1] = 1; + mScrToImg.mapVectors(point); + boolean swapxy = (point[0] > 0.0f); + + int sign = 1; + + float dx = x - mDownX; + float dy = y - mDownY; + switch (handle) { + case HAN_CENTER: + if (centerIsOutside(mDownPoint1X + dx, mDownPoint1Y + dy, + mDownPoint2X + dx, mDownPoint2Y + dy)) { + break; + } + line.setPoint1(mDownPoint1X + dx, mDownPoint1Y + dy); + line.setPoint2(mDownPoint2X + dx, mDownPoint2Y + dy); + break; + case HAN_SOUTH: + if (centerIsOutside(mDownPoint1X + dx, mDownPoint1Y + dy, + mDownPoint2X, mDownPoint2Y)) { + break; + } + line.setPoint1(mDownPoint1X + dx, mDownPoint1Y + dy); + break; + case HAN_NORTH: + if (centerIsOutside(mDownPoint1X, mDownPoint1Y, + mDownPoint2X + dx, mDownPoint2Y + dy)) { + break; + } + line.setPoint2(mDownPoint2X + dx, mDownPoint2Y + dy); + break; + } + } + + public void paintGrayPoint(Canvas canvas, float x, float y) { + if (isUndefined()) { + return; + } + + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + RadialGradient g = new RadialGradient(x, y, mCenterDotSize, mGrayPointColorPatern, + mPointRadialPos, Shader.TileMode.CLAMP); + paint.setShader(g); + canvas.drawCircle(x, y, mCenterDotSize, paint); + } + + public void paintPoint(Canvas canvas, float x, float y) { + if (isUndefined()) { + return; + } + + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + RadialGradient g = new RadialGradient(x, y, mCenterDotSize, mPointColorPatern, + mPointRadialPos, Shader.TileMode.CLAMP); + paint.setShader(g); + canvas.drawCircle(x, y, mCenterDotSize, paint); + } + + void paintLines(Canvas canvas, float p1x, float p1y, float p2x, float p2y) { + if (isUndefined()) { + return; + } + + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.STROKE); + + mPaint.setStrokeWidth(6); + mPaint.setColor(mlineShadowColor); + mPaint.setPathEffect(mDash); + paintOvallines(canvas, mPaint, p1x, p1y, p2x, p2y); + + mPaint.setStrokeWidth(3); + mPaint.setColor(mLineColor); + mPaint.setPathEffect(mDash); + paintOvallines(canvas, mPaint, p1x, p1y, p2x, p2y); + } + + public void paintOvallines( + Canvas canvas, Paint paint, float p1x, float p1y, float p2x, float p2y) { + + + + canvas.drawLine(p1x, p1y, p2x, p2y, paint); + + float cx = (p1x + p2x) / 2; + float cy = (p1y + p2y) / 2; + float dx = p1x - p2x; + float dy = p1y - p2y; + float len = (float) Math.sqrt(dx * dx + dy * dy); + dx *= 2048 / len; + dy *= 2048 / len; + + canvas.drawLine(p1x + dy, p1y - dx, p1x - dy, p1y + dx, paint); + canvas.drawLine(p2x + dy, p2y - dx, p2x - dy, p2y + dx, paint); + } + + public void fillHandles(Canvas canvas, float p1x, float p1y, float p2x, float p2y) { + float cx = (p1x + p2x) / 2; + float cy = (p1y + p2y) / 2; + handlex[0] = cx; + handley[0] = cy; + handlex[1] = p1x; + handley[1] = p1y; + handlex[2] = p2x; + handley[2] = p2y; + + } + + public void draw(Canvas canvas) { + paintLines(canvas, mPoint1X, mPoint1Y, mPoint2X, mPoint2Y); + fillHandles(canvas, mPoint1X, mPoint1Y, mPoint2X, mPoint2Y); + paintPoint(canvas, mPoint2X, mPoint2Y); + paintPoint(canvas, mPoint1X, mPoint1Y); + paintPoint(canvas, (mPoint1X + mPoint2X) / 2, (mPoint1Y + mPoint2Y) / 2); + } + + public boolean isUndefined() { + return Float.isNaN(mPoint1X); + } + + public void setShowReshapeHandles(boolean showReshapeHandles) { + this.mShowReshapeHandles = showReshapeHandles; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java new file mode 100644 index 000000000..7fee03188 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.crop.CropDrawingUtils; +import com.android.gallery3d.filtershow.crop.CropMath; +import com.android.gallery3d.filtershow.crop.CropObject; +import com.android.gallery3d.filtershow.editors.EditorCrop; +import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; + +public class ImageCrop extends ImageShow { + private static final String TAG = ImageCrop.class.getSimpleName(); + private RectF mImageBounds = new RectF(); + private RectF mScreenCropBounds = new RectF(); + private Paint mPaint = new Paint(); + private CropObject mCropObj = null; + private GeometryHolder mGeometry = new GeometryHolder(); + private GeometryHolder mUpdateHolder = new GeometryHolder(); + private Drawable mCropIndicator; + private int mIndicatorSize; + private boolean mMovingBlock = false; + private Matrix mDisplayMatrix = null; + private Matrix mDisplayCropMatrix = null; + private Matrix mDisplayMatrixInverse = null; + private float mPrevX = 0; + private float mPrevY = 0; + private int mMinSideSize = 90; + private int mTouchTolerance = 40; + private enum Mode { + NONE, MOVE + } + private Mode mState = Mode.NONE; + private boolean mValidDraw = false; + FilterCropRepresentation mLocalRep = new FilterCropRepresentation(); + EditorCrop mEditorCrop; + + public ImageCrop(Context context) { + super(context); + setup(context); + } + + public ImageCrop(Context context, AttributeSet attrs) { + super(context, attrs); + setup(context); + } + + public ImageCrop(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setup(context); + } + + private void setup(Context context) { + Resources rsc = context.getResources(); + mCropIndicator = rsc.getDrawable(R.drawable.camera_crop); + mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size); + mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side); + mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance); + } + + public void setFilterCropRepresentation(FilterCropRepresentation crop) { + mLocalRep = (crop == null) ? new FilterCropRepresentation() : crop; + GeometryMathUtils.initializeHolder(mUpdateHolder, mLocalRep); + mValidDraw = true; + } + + public FilterCropRepresentation getFinalRepresentation() { + return mLocalRep; + } + + private void internallyUpdateLocalRep(RectF crop, RectF image) { + FilterCropRepresentation + .findNormalizedCrop(crop, (int) image.width(), (int) image.height()); + mGeometry.crop.set(crop); + mUpdateHolder.set(mGeometry); + mLocalRep.setCrop(crop); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { + return true; + } + float[] touchPoint = { + x, y + }; + mDisplayMatrixInverse.mapPoints(touchPoint); + x = touchPoint[0]; + y = touchPoint[1]; + switch (event.getActionMasked()) { + case (MotionEvent.ACTION_DOWN): + if (mState == Mode.NONE) { + if (!mCropObj.selectEdge(x, y)) { + mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK); + } + mPrevX = x; + mPrevY = y; + mState = Mode.MOVE; + } + break; + case (MotionEvent.ACTION_UP): + if (mState == Mode.MOVE) { + mCropObj.selectEdge(CropObject.MOVE_NONE); + mMovingBlock = false; + mPrevX = x; + mPrevY = y; + mState = Mode.NONE; + internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); + } + break; + case (MotionEvent.ACTION_MOVE): + if (mState == Mode.MOVE) { + float dx = x - mPrevX; + float dy = y - mPrevY; + mCropObj.moveCurrentSelection(dx, dy); + mPrevX = x; + mPrevY = y; + } + break; + default: + break; + } + invalidate(); + return true; + } + + private void clearDisplay() { + mDisplayMatrix = null; + mDisplayMatrixInverse = null; + invalidate(); + } + + public void applyFreeAspect() { + mCropObj.unsetAspectRatio(); + invalidate(); + } + + public void applyOriginalAspect() { + RectF outer = mCropObj.getOuterBounds(); + float w = outer.width(); + float h = outer.height(); + if (w > 0 && h > 0) { + applyAspect(w, h); + mCropObj.resetBoundsTo(outer, outer); + internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); + } else { + Log.w(TAG, "failed to set aspect ratio original"); + } + invalidate(); + } + + public void applyAspect(float x, float y) { + if (x <= 0 || y <= 0) { + throw new IllegalArgumentException("Bad arguments to applyAspect"); + } + // If we are rotated by 90 degrees from horizontal, swap x and y + if (GeometryMathUtils.needsDimensionSwap(mGeometry.rotation)) { + float tmp = x; + x = y; + y = tmp; + } + if (!mCropObj.setInnerAspectRatio(x, y)) { + Log.w(TAG, "failed to set aspect ratio"); + } + internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); + invalidate(); + } + + /** + * Rotates first d bits in integer x to the left some number of times. + */ + private int bitCycleLeft(int x, int times, int d) { + int mask = (1 << d) - 1; + int mout = x & mask; + times %= d; + int hi = mout >> (d - times); + int low = (mout << times) & mask; + int ret = x & ~mask; + ret |= low; + ret |= hi; + return ret; + } + + /** + * Find the selected edge or corner in screen coordinates. + */ + private int decode(int movingEdges, float rotation) { + int rot = CropMath.constrainedRotation(rotation); + switch (rot) { + case 90: + return bitCycleLeft(movingEdges, 1, 4); + case 180: + return bitCycleLeft(movingEdges, 2, 4); + case 270: + return bitCycleLeft(movingEdges, 3, 4); + default: + return movingEdges; + } + } + + private void forceStateConsistency() { + MasterImage master = MasterImage.getImage(); + Bitmap image = master.getFiltersOnlyImage(); + int width = image.getWidth(); + int height = image.getHeight(); + if (mCropObj == null || !mUpdateHolder.equals(mGeometry) + || mImageBounds.width() != width || mImageBounds.height() != height + || !mLocalRep.getCrop().equals(mUpdateHolder.crop)) { + mImageBounds.set(0, 0, width, height); + mGeometry.set(mUpdateHolder); + mLocalRep.setCrop(mUpdateHolder.crop); + RectF scaledCrop = new RectF(mUpdateHolder.crop); + FilterCropRepresentation.findScaledCrop(scaledCrop, width, height); + mCropObj = new CropObject(mImageBounds, scaledCrop, (int) mUpdateHolder.straighten); + mState = Mode.NONE; + clearDisplay(); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + clearDisplay(); + } + + @Override + public void onDraw(Canvas canvas) { + Bitmap bitmap = MasterImage.getImage().getFiltersOnlyImage(); + if (!mValidDraw || bitmap == null) { + return; + } + forceStateConsistency(); + mImageBounds.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); + // If display matrix doesn't exist, create it and its dependencies + if (mDisplayCropMatrix == null || mDisplayMatrix == null || mDisplayMatrixInverse == null) { + mDisplayMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry, + bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight()); + float straighten = mGeometry.straighten; + mGeometry.straighten = 0; + mDisplayCropMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry, + bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight()); + mGeometry.straighten = straighten; + mDisplayMatrixInverse = new Matrix(); + mDisplayMatrixInverse.reset(); + if (!mDisplayCropMatrix.invert(mDisplayMatrixInverse)) { + Log.w(TAG, "could not invert display matrix"); + mDisplayMatrixInverse = null; + return; + } + // Scale min side and tolerance by display matrix scale factor + mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); + mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); + } + // Draw actual bitmap + mPaint.reset(); + mPaint.setAntiAlias(true); + mPaint.setFilterBitmap(true); + canvas.drawBitmap(bitmap, mDisplayMatrix, mPaint); + mCropObj.getInnerBounds(mScreenCropBounds); + RectF outer = mCropObj.getOuterBounds(); + FilterCropRepresentation.findNormalizedCrop(mScreenCropBounds, (int) outer.width(), + (int) outer.height()); + FilterCropRepresentation.findScaledCrop(mScreenCropBounds, bitmap.getWidth(), + bitmap.getHeight()); + if (mDisplayCropMatrix.mapRect(mScreenCropBounds)) { + // Draw crop rect and markers + CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds); + CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds); + CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, + mScreenCropBounds, mCropObj.isFixedAspect(), + decode(mCropObj.getSelectState(), mGeometry.rotation.value())); + } + } + + public void setEditor(EditorCrop editorCrop) { + mEditorCrop = editorCrop; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java new file mode 100644 index 000000000..82c4b2fc7 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +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.os.AsyncTask; +import android.util.AttributeSet; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.PopupMenu; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.Editor; +import com.android.gallery3d.filtershow.editors.EditorCurves; +import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.filters.ImageFilterCurves; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; + +import java.util.HashMap; + +public class ImageCurves extends ImageShow { + + private static final String LOGTAG = "ImageCurves"; + Paint gPaint = new Paint(); + Path gPathSpline = new Path(); + HashMap<Integer, String> mIdStrLut; + + private int mCurrentCurveIndex = Spline.RGB; + private boolean mDidAddPoint = false; + private boolean mDidDelete = false; + private ControlPoint mCurrentControlPoint = null; + private int mCurrentPick = -1; + private ImagePreset mLastPreset = null; + int[] redHistogram = new int[256]; + int[] greenHistogram = new int[256]; + int[] blueHistogram = new int[256]; + Path gHistoPath = new Path(); + + boolean mDoingTouchMove = false; + private EditorCurves mEditorCurves; + private FilterCurvesRepresentation mFilterCurvesRepresentation; + + public ImageCurves(Context context) { + super(context); + setLayerType(LAYER_TYPE_SOFTWARE, gPaint); + resetCurve(); + } + + public ImageCurves(Context context, AttributeSet attrs) { + super(context, attrs); + setLayerType(LAYER_TYPE_SOFTWARE, gPaint); + resetCurve(); + } + + @Override + protected boolean enableComparison() { + return false; + } + + @Override + public boolean useUtilityPanel() { + return true; + } + + private void showPopupMenu(LinearLayout accessoryViewList) { + final Button button = (Button) accessoryViewList.findViewById( + R.id.applyEffect); + if (button == null) { + return; + } + if (mIdStrLut == null){ + mIdStrLut = new HashMap<Integer, String>(); + mIdStrLut.put(R.id.curve_menu_rgb, + getContext().getString(R.string.curves_channel_rgb)); + mIdStrLut.put(R.id.curve_menu_red, + getContext().getString(R.string.curves_channel_red)); + mIdStrLut.put(R.id.curve_menu_green, + getContext().getString(R.string.curves_channel_green)); + mIdStrLut.put(R.id.curve_menu_blue, + getContext().getString(R.string.curves_channel_blue)); + } + PopupMenu popupMenu = new PopupMenu(getActivity(), button); + popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_curves, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + setChannel(item.getItemId()); + button.setText(mIdStrLut.get(item.getItemId())); + return true; + } + }); + Editor.hackFixStrings(popupMenu.getMenu()); + popupMenu.show(); + } + + @Override + public void openUtilityPanel(final LinearLayout accessoryViewList) { + Context context = accessoryViewList.getContext(); + Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect); + view.setText(context.getString(R.string.curves_channel_rgb)); + view.setVisibility(View.VISIBLE); + + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View arg0) { + showPopupMenu(accessoryViewList); + } + }); + + if (view != null) { + view.setVisibility(View.VISIBLE); + } + } + + public void nextChannel() { + mCurrentCurveIndex = ((mCurrentCurveIndex + 1) % 4); + invalidate(); + } + + private ImageFilterCurves curves() { + String filterName = getFilterName(); + ImagePreset p = getImagePreset(); + if (p != null) { + return (ImageFilterCurves) FiltersManager.getManager().getFilter(ImageFilterCurves.class); + } + return null; + } + + private Spline getSpline(int index) { + return mFilterCurvesRepresentation.getSpline(index); + } + + @Override + public void resetParameter() { + super.resetParameter(); + resetCurve(); + mLastPreset = null; + invalidate(); + } + + public void resetCurve() { + if (mFilterCurvesRepresentation != null) { + mFilterCurvesRepresentation.reset(); + updateCachedImage(); + } + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mFilterCurvesRepresentation == null) { + return; + } + + gPaint.setAntiAlias(true); + + if (getImagePreset() != mLastPreset && getFilteredImage() != null) { + new ComputeHistogramTask().execute(getFilteredImage()); + mLastPreset = getImagePreset(); + } + + if (curves() == null) { + return; + } + + if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.RED) { + drawHistogram(canvas, redHistogram, Color.RED, PorterDuff.Mode.SCREEN); + } + if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.GREEN) { + drawHistogram(canvas, greenHistogram, Color.GREEN, PorterDuff.Mode.SCREEN); + } + if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.BLUE) { + drawHistogram(canvas, blueHistogram, Color.BLUE, PorterDuff.Mode.SCREEN); + } + // We only display the other channels curves when showing the RGB curve + if (mCurrentCurveIndex == Spline.RGB) { + for (int i = 0; i < 4; i++) { + Spline spline = getSpline(i); + if (i != mCurrentCurveIndex && !spline.isOriginal()) { + // And we only display a curve if it has more than two + // points + spline.draw(canvas, Spline.colorForCurve(i), getWidth(), + getHeight(), false, mDoingTouchMove); + } + } + } + // ...but we always display the current curve. + getSpline(mCurrentCurveIndex) + .draw(canvas, Spline.colorForCurve(mCurrentCurveIndex), getWidth(), getHeight(), + true, mDoingTouchMove); + + } + + private int pickControlPoint(float x, float y) { + int pick = 0; + Spline spline = getSpline(mCurrentCurveIndex); + float px = spline.getPoint(0).x; + float py = spline.getPoint(0).y; + double delta = Math.sqrt((px - x) * (px - x) + (py - y) * (py - y)); + for (int i = 1; i < spline.getNbPoints(); i++) { + px = spline.getPoint(i).x; + py = spline.getPoint(i).y; + double currentDelta = Math.sqrt((px - x) * (px - x) + (py - y) + * (py - y)); + if (currentDelta < delta) { + delta = currentDelta; + pick = i; + } + } + + if (!mDidAddPoint && (delta * getWidth() > 100) + && (spline.getNbPoints() < 10)) { + return -1; + } + + return pick; + } + + private String getFilterName() { + return "Curves"; + } + + @Override + public synchronized boolean onTouchEvent(MotionEvent e) { + if (e.getPointerCount() != 1) { + return true; + } + + if (didFinishScalingOperation()) { + return true; + } + + float margin = Spline.curveHandleSize() / 2; + float posX = e.getX(); + if (posX < margin) { + posX = margin; + } + float posY = e.getY(); + if (posY < margin) { + posY = margin; + } + if (posX > getWidth() - margin) { + posX = getWidth() - margin; + } + if (posY > getHeight() - margin) { + posY = getHeight() - margin; + } + posX = (posX - margin) / (getWidth() - 2 * margin); + posY = (posY - margin) / (getHeight() - 2 * margin); + + if (e.getActionMasked() == MotionEvent.ACTION_UP) { + mCurrentControlPoint = null; + mCurrentPick = -1; + updateCachedImage(); + mDidAddPoint = false; + if (mDidDelete) { + mDidDelete = false; + } + mDoingTouchMove = false; + return true; + } + + if (mDidDelete) { + return true; + } + + if (curves() == null) { + return true; + } + + if (e.getActionMasked() == MotionEvent.ACTION_MOVE) { + mDoingTouchMove = true; + Spline spline = getSpline(mCurrentCurveIndex); + int pick = mCurrentPick; + if (mCurrentControlPoint == null) { + pick = pickControlPoint(posX, posY); + if (pick == -1) { + mCurrentControlPoint = new ControlPoint(posX, posY); + pick = spline.addPoint(mCurrentControlPoint); + mDidAddPoint = true; + } else { + mCurrentControlPoint = spline.getPoint(pick); + } + mCurrentPick = pick; + } + + if (spline.isPointContained(posX, pick)) { + spline.movePoint(pick, posX, posY); + } else if (pick != -1 && spline.getNbPoints() > 2) { + spline.deletePoint(pick); + mDidDelete = true; + } + updateCachedImage(); + invalidate(); + } + return true; + } + + public synchronized void updateCachedImage() { + if (getImagePreset() != null) { + resetImageCaches(this); + if (mEditorCurves != null) { + mEditorCurves.commitLocalRepresentation(); + } + invalidate(); + } + } + + class ComputeHistogramTask extends AsyncTask<Bitmap, Void, int[]> { + @Override + protected int[] doInBackground(Bitmap... params) { + int[] histo = new int[256 * 3]; + Bitmap bitmap = params[0]; + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int[] pixels = new int[w * h]; + bitmap.getPixels(pixels, 0, w, 0, 0, w, h); + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + int index = j * w + i; + int r = Color.red(pixels[index]); + int g = Color.green(pixels[index]); + int b = Color.blue(pixels[index]); + histo[r]++; + histo[256 + g]++; + histo[512 + b]++; + } + } + return histo; + } + + @Override + protected void onPostExecute(int[] result) { + System.arraycopy(result, 0, redHistogram, 0, 256); + System.arraycopy(result, 256, greenHistogram, 0, 256); + System.arraycopy(result, 512, blueHistogram, 0, 256); + invalidate(); + } + } + + private void drawHistogram(Canvas canvas, int[] histogram, int color, PorterDuff.Mode mode) { + int max = 0; + for (int i = 0; i < histogram.length; i++) { + if (histogram[i] > max) { + max = histogram[i]; + } + } + float w = getWidth() - Spline.curveHandleSize(); + float h = getHeight() - Spline.curveHandleSize() / 2.0f; + float dx = Spline.curveHandleSize() / 2.0f; + float wl = w / histogram.length; + float wh = (0.3f * h) / max; + Paint paint = new Paint(); + paint.setARGB(100, 255, 255, 255); + paint.setStrokeWidth((int) Math.ceil(wl)); + + Paint paint2 = new Paint(); + paint2.setColor(color); + paint2.setStrokeWidth(6); + paint2.setXfermode(new PorterDuffXfermode(mode)); + gHistoPath.reset(); + gHistoPath.moveTo(dx, h); + boolean firstPointEncountered = false; + float prev = 0; + float last = 0; + for (int i = 0; i < histogram.length; i++) { + float x = i * wl + dx; + float l = histogram[i] * wh; + if (l != 0) { + float v = h - (l + prev) / 2.0f; + if (!firstPointEncountered) { + gHistoPath.lineTo(x, h); + firstPointEncountered = true; + } + gHistoPath.lineTo(x, v); + prev = l; + last = x; + } + } + gHistoPath.lineTo(last, h); + gHistoPath.lineTo(w, h); + gHistoPath.close(); + canvas.drawPath(gHistoPath, paint2); + paint2.setStrokeWidth(2); + paint2.setStyle(Paint.Style.STROKE); + paint2.setARGB(255, 200, 200, 200); + canvas.drawPath(gHistoPath, paint2); + } + + public void setChannel(int itemId) { + switch (itemId) { + case R.id.curve_menu_rgb: { + mCurrentCurveIndex = Spline.RGB; + break; + } + case R.id.curve_menu_red: { + mCurrentCurveIndex = Spline.RED; + break; + } + case R.id.curve_menu_green: { + mCurrentCurveIndex = Spline.GREEN; + break; + } + case R.id.curve_menu_blue: { + mCurrentCurveIndex = Spline.BLUE; + break; + } + } + mEditorCurves.commitLocalRepresentation(); + invalidate(); + } + + public void setEditor(EditorCurves editorCurves) { + mEditorCurves = editorCurves; + } + + public void setFilterDrawRepresentation(FilterCurvesRepresentation drawRep) { + mFilterCurvesRepresentation = drawRep; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java new file mode 100644 index 000000000..9722034e0 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java @@ -0,0 +1,139 @@ + +package com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorDraw; +import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation; +import com.android.gallery3d.filtershow.filters.ImageFilterDraw; + +public class ImageDraw extends ImageShow { + + private static final String LOGTAG = "ImageDraw"; + private int mCurrentColor = Color.RED; + final static float INITAL_STROKE_RADIUS = 40; + private float mCurrentSize = INITAL_STROKE_RADIUS; + private byte mType = 0; + private FilterDrawRepresentation mFRep; + private EditorDraw mEditorDraw; + + public ImageDraw(Context context, AttributeSet attrs) { + super(context, attrs); + resetParameter(); + } + + public ImageDraw(Context context) { + super(context); + resetParameter(); + } + + public void setEditor(EditorDraw editorDraw) { + mEditorDraw = editorDraw; + } + public void setFilterDrawRepresentation(FilterDrawRepresentation fr) { + mFRep = fr; + } + + public Drawable getIcon(Context context) { + + return null; + } + + @Override + public void resetParameter() { + if (mFRep != null) { + mFRep.clear(); + } + } + + public void setColor(int color) { + mCurrentColor = color; + } + + public void setSize(int size) { + mCurrentSize = size; + } + + public void setStyle(byte style) { + mType = (byte) (style % ImageFilterDraw.NUMBER_OF_STYLES); + } + + public int getStyle() { + return mType; + } + + public int getSize() { + return (int) mCurrentSize; + } + + float[] mTmpPoint = new float[2]; // so we do not malloc + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getPointerCount() > 1) { + boolean ret = super.onTouchEvent(event); + if (mFRep.getCurrentDrawing() != null) { + mFRep.clearCurrentSection(); + mEditorDraw.commitLocalRepresentation(); + } + return ret; + } + if (event.getAction() != MotionEvent.ACTION_DOWN) { + if (mFRep.getCurrentDrawing() == null) { + return super.onTouchEvent(event); + } + } + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + calcScreenMapping(); + mTmpPoint[0] = event.getX(); + mTmpPoint[1] = event.getY(); + mToOrig.mapPoints(mTmpPoint); + mFRep.startNewSection(mType, mCurrentColor, mCurrentSize, mTmpPoint[0], mTmpPoint[1]); + } + + if (event.getAction() == MotionEvent.ACTION_MOVE) { + + int historySize = event.getHistorySize(); + for (int h = 0; h < historySize; h++) { + int p = 0; + { + mTmpPoint[0] = event.getHistoricalX(p, h); + mTmpPoint[1] = event.getHistoricalY(p, h); + mToOrig.mapPoints(mTmpPoint); + mFRep.addPoint(mTmpPoint[0], mTmpPoint[1]); + } + } + } + + if (event.getAction() == MotionEvent.ACTION_UP) { + mTmpPoint[0] = event.getX(); + mTmpPoint[1] = event.getY(); + mToOrig.mapPoints(mTmpPoint); + mFRep.endSection(mTmpPoint[0], mTmpPoint[1]); + } + mEditorDraw.commitLocalRepresentation(); + invalidate(); + return true; + } + + Matrix mRotateToScreen = new Matrix(); + Matrix mToOrig; + private void calcScreenMapping() { + mToOrig = getScreenToImageMatrix(true); + mToOrig.invert(mRotateToScreen); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + calcScreenMapping(); + + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java b/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java new file mode 100644 index 000000000..b55cc2bc4 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java @@ -0,0 +1,215 @@ +package com.android.gallery3d.filtershow.imageshow; +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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. + */ + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.editors.EditorGrad; +import com.android.gallery3d.filtershow.filters.FilterGradRepresentation; + +public class ImageGrad extends ImageShow { + private static final String LOGTAG = "ImageGrad"; + private FilterGradRepresentation mGradRep; + private EditorGrad mEditorGrad; + private float mMinTouchDist; + private int mActiveHandle = -1; + private GradControl mEllipse; + + Matrix mToScr = new Matrix(); + float[] mPointsX = new float[FilterGradRepresentation.MAX_POINTS]; + float[] mPointsY = new float[FilterGradRepresentation.MAX_POINTS]; + + public ImageGrad(Context context) { + super(context); + Resources res = context.getResources(); + mMinTouchDist = res.getDimensionPixelSize(R.dimen.gradcontrol_min_touch_dist); + mEllipse = new GradControl(context); + mEllipse.setShowReshapeHandles(false); + } + + public ImageGrad(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = context.getResources(); + mMinTouchDist = res.getDimensionPixelSize(R.dimen.gradcontrol_min_touch_dist); + mEllipse = new GradControl(context); + mEllipse.setShowReshapeHandles(false); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int mask = event.getActionMasked(); + + if (mActiveHandle == -1) { + if (MotionEvent.ACTION_DOWN != mask) { + return super.onTouchEvent(event); + } + if (event.getPointerCount() == 1) { + mActiveHandle = mEllipse.getCloseHandle(event.getX(), event.getY()); + if (mActiveHandle == -1) { + float x = event.getX(); + float y = event.getY(); + float min_d = Float.MAX_VALUE; + int pos = -1; + for (int i = 0; i < mPointsX.length; i++) { + if (mPointsX[i] == -1) { + continue; + } + float d = (float) Math.hypot(x - mPointsX[i], y - mPointsY[i]); + if ( min_d > d) { + min_d = d; + pos = i; + } + } + if (min_d > mMinTouchDist){ + pos = -1; + } + + if (pos != -1) { + mGradRep.setSelectedPoint(pos); + resetImageCaches(this); + mEditorGrad.updateSeekBar(mGradRep); + mEditorGrad.commitLocalRepresentation(); + invalidate(); + } + } + } + if (mActiveHandle == -1) { + return super.onTouchEvent(event); + } + } else { + switch (mask) { + case MotionEvent.ACTION_UP: { + + mActiveHandle = -1; + break; + } + case MotionEvent.ACTION_DOWN: { + break; + } + } + } + float x = event.getX(); + float y = event.getY(); + + mEllipse.setScrImageInfo(getScreenToImageMatrix(true), + MasterImage.getImage().getOriginalBounds()); + + switch (mask) { + case (MotionEvent.ACTION_DOWN): { + mEllipse.actionDown(x, y, mGradRep); + break; + } + case (MotionEvent.ACTION_UP): + case (MotionEvent.ACTION_MOVE): { + mEllipse.actionMove(mActiveHandle, x, y, mGradRep); + setRepresentation(mGradRep); + break; + } + } + invalidate(); + mEditorGrad.commitLocalRepresentation(); + return true; + } + + public void setRepresentation(FilterGradRepresentation pointRep) { + mGradRep = pointRep; + Matrix toImg = getScreenToImageMatrix(false); + + toImg.invert(mToScr); + + float[] c1 = new float[] { mGradRep.getPoint1X(), mGradRep.getPoint1Y() }; + float[] c2 = new float[] { mGradRep.getPoint2X(), mGradRep.getPoint2Y() }; + + if (c1[0] == -1) { + float cx = MasterImage.getImage().getOriginalBounds().width() / 2; + float cy = MasterImage.getImage().getOriginalBounds().height() / 2; + float rx = Math.min(cx, cy) * .4f; + + mGradRep.setPoint1(cx, cy-rx); + mGradRep.setPoint2(cx, cy+rx); + c1[0] = cx; + c1[1] = cy-rx; + mToScr.mapPoints(c1); + if (getWidth() != 0) { + mEllipse.setPoint1(c1[0], c1[1]); + c2[0] = cx; + c2[1] = cy+rx; + mToScr.mapPoints(c2); + mEllipse.setPoint2(c2[0], c2[1]); + } + mEditorGrad.commitLocalRepresentation(); + } else { + mToScr.mapPoints(c1); + mToScr.mapPoints(c2); + mEllipse.setPoint1(c1[0], c1[1]); + mEllipse.setPoint2(c2[0], c2[1]); + } + } + + public void drawOtherPoints(Canvas canvas) { + computCenterLocations(); + for (int i = 0; i < mPointsX.length; i++) { + if (mPointsX[i] != -1) { + mEllipse.paintGrayPoint(canvas, mPointsX[i], mPointsY[i]); + } + } + } + + public void computCenterLocations() { + int x1[] = mGradRep.getXPos1(); + int y1[] = mGradRep.getYPos1(); + int x2[] = mGradRep.getXPos2(); + int y2[] = mGradRep.getYPos2(); + int selected = mGradRep.getSelectedPoint(); + boolean m[] = mGradRep.getMask(); + float[] c = new float[2]; + for (int i = 0; i < m.length; i++) { + if (selected == i || !m[i]) { + mPointsX[i] = -1; + continue; + } + + c[0] = (x1[i]+x2[i])/2; + c[1] = (y1[i]+y2[i])/2; + mToScr.mapPoints(c); + + mPointsX[i] = c[0]; + mPointsY[i] = c[1]; + } + } + + public void setEditor(EditorGrad editorGrad) { + mEditorGrad = editorGrad; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mGradRep == null) { + return; + } + setRepresentation(mGradRep); + mEllipse.draw(canvas); + drawOtherPoints(canvas); + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java b/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java new file mode 100644 index 000000000..26c49b1a8 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorMirror; +import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; + +public class ImageMirror extends ImageShow { + private static final String TAG = ImageMirror.class.getSimpleName(); + private EditorMirror mEditorMirror; + private FilterMirrorRepresentation mLocalRep = new FilterMirrorRepresentation(); + private GeometryHolder mDrawHolder = new GeometryHolder(); + + public ImageMirror(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ImageMirror(Context context) { + super(context); + } + + public void setFilterMirrorRepresentation(FilterMirrorRepresentation rep) { + mLocalRep = (rep == null) ? new FilterMirrorRepresentation() : rep; + } + + public void flip() { + mLocalRep.cycle(); + invalidate(); + } + + public FilterMirrorRepresentation getFinalRepresentation() { + return mLocalRep; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Treat event as handled. + return true; + } + + @Override + public void onDraw(Canvas canvas) { + MasterImage master = MasterImage.getImage(); + Bitmap image = master.getFiltersOnlyImage(); + if (image == null) { + return; + } + GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep); + GeometryMathUtils.drawTransformedCropped(mDrawHolder, canvas, image, getWidth(), + getHeight()); + } + + public void setEditor(EditorMirror editorFlip) { + mEditorMirror = editorFlip; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java new file mode 100644 index 000000000..fd5714139 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.util.AttributeSet; + +import com.android.gallery3d.filtershow.editors.EditorRedEye; +import com.android.gallery3d.filtershow.filters.FilterPoint; +import com.android.gallery3d.filtershow.filters.FilterRedEyeRepresentation; +import com.android.gallery3d.filtershow.filters.ImageFilterRedEye; + +public abstract class ImagePoint extends ImageShow { + + private static final String LOGTAG = "ImageRedEyes"; + protected EditorRedEye mEditorRedEye; + protected FilterRedEyeRepresentation mRedEyeRep; + protected static float mTouchPadding = 80; + + public static void setTouchPadding(float padding) { + mTouchPadding = padding; + } + + public ImagePoint(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ImagePoint(Context context) { + super(context); + } + + @Override + public void resetParameter() { + ImageFilterRedEye filter = (ImageFilterRedEye) getCurrentFilter(); + if (filter != null) { + filter.clear(); + } + invalidate(); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + Paint paint = new Paint(); + paint.setStyle(Style.STROKE); + paint.setColor(Color.RED); + paint.setStrokeWidth(2); + + Matrix originalToScreen = getImageToScreenMatrix(false); + Matrix originalRotateToScreen = getImageToScreenMatrix(true); + + if (mRedEyeRep != null) { + for (FilterPoint candidate : mRedEyeRep.getCandidates()) { + drawPoint(candidate, canvas, originalToScreen, originalRotateToScreen, paint); + } + } + } + + protected abstract void drawPoint( + FilterPoint candidate, Canvas canvas, Matrix originalToScreen, + Matrix originalRotateToScreen, Paint paint); + + public void setEditor(EditorRedEye editorRedEye) { + mEditorRedEye = editorRedEye; + } + + public void setRepresentation(FilterRedEyeRepresentation redEyeRep) { + mRedEyeRep = redEyeRep; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java new file mode 100644 index 000000000..40433a02e --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.filters.FilterPoint; +import com.android.gallery3d.filtershow.filters.RedEyeCandidate; + +public class ImageRedEye extends ImagePoint { + private static final String LOGTAG = "ImageRedEyes"; + private RectF mCurrentRect = null; + + public ImageRedEye(Context context) { + super(context); + } + + @Override + public void resetParameter() { + super.resetParameter(); + invalidate(); + } + + @Override + + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + if (event.getPointerCount() > 1) { + return true; + } + + if (didFinishScalingOperation()) { + return true; + } + + float ex = event.getX(); + float ey = event.getY(); + + // let's transform (ex, ey) to displayed image coordinates + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mCurrentRect = new RectF(); + mCurrentRect.left = ex - mTouchPadding; + mCurrentRect.top = ey - mTouchPadding; + } + if (event.getAction() == MotionEvent.ACTION_MOVE) { + mCurrentRect.right = ex + mTouchPadding; + mCurrentRect.bottom = ey + mTouchPadding; + } + if (event.getAction() == MotionEvent.ACTION_UP) { + if (mCurrentRect != null) { + // transform to original coordinates + Matrix originalNoRotateToScreen = getImageToScreenMatrix(false); + Matrix originalToScreen = getImageToScreenMatrix(true); + Matrix invert = new Matrix(); + originalToScreen.invert(invert); + RectF r = new RectF(mCurrentRect); + invert.mapRect(r); + RectF r2 = new RectF(mCurrentRect); + invert.reset(); + originalNoRotateToScreen.invert(invert); + invert.mapRect(r2); + mRedEyeRep.addRect(r, r2); + this.resetImageCaches(this); + } + mCurrentRect = null; + } + mEditorRedEye.commitLocalRepresentation(); + invalidate(); + return true; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + Paint paint = new Paint(); + paint.setStyle(Style.STROKE); + paint.setColor(Color.RED); + paint.setStrokeWidth(2); + if (mCurrentRect != null) { + paint.setColor(Color.RED); + RectF drawRect = new RectF(mCurrentRect); + canvas.drawRect(drawRect, paint); + } + } + + @Override + protected void drawPoint(FilterPoint point, Canvas canvas, Matrix originalToScreen, + Matrix originalRotateToScreen, Paint paint) { + RedEyeCandidate candidate = (RedEyeCandidate) point; + RectF rect = candidate.getRect(); + RectF drawRect = new RectF(); + originalToScreen.mapRect(drawRect, rect); + RectF fullRect = new RectF(); + originalRotateToScreen.mapRect(fullRect, rect); + paint.setColor(Color.BLUE); + canvas.drawRect(fullRect, paint); + canvas.drawLine(fullRect.centerX(), fullRect.top, + fullRect.centerX(), fullRect.bottom, paint); + canvas.drawLine(fullRect.left, fullRect.centerY(), + fullRect.right, fullRect.centerY(), paint); + paint.setColor(Color.GREEN); + float dw = drawRect.width(); + float dh = drawRect.height(); + float dx = fullRect.centerX() - dw / 2; + float dy = fullRect.centerY() - dh / 2; + drawRect.set(dx, dy, dx + dw, dy + dh); + canvas.drawRect(drawRect, paint); + canvas.drawLine(drawRect.centerX(), drawRect.top, + drawRect.centerX(), drawRect.bottom, paint); + canvas.drawLine(drawRect.left, drawRect.centerY(), + drawRect.right, drawRect.centerY(), paint); + canvas.drawCircle(drawRect.centerX(), drawRect.centerY(), + mTouchPadding, paint); + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java new file mode 100644 index 000000000..5186c09d7 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorRotate; +import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; + +public class ImageRotate extends ImageShow { + private EditorRotate mEditorRotate; + private static final String TAG = ImageRotate.class.getSimpleName(); + private FilterRotateRepresentation mLocalRep = new FilterRotateRepresentation(); + private GeometryHolder mDrawHolder = new GeometryHolder(); + + public ImageRotate(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ImageRotate(Context context) { + super(context); + } + + public void setFilterRotateRepresentation(FilterRotateRepresentation rep) { + mLocalRep = (rep == null) ? new FilterRotateRepresentation() : rep; + } + + public void rotate() { + mLocalRep.rotateCW(); + invalidate(); + } + + public FilterRotateRepresentation getFinalRepresentation() { + return mLocalRep; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Treat event as handled. + return true; + } + + public int getLocalValue() { + return mLocalRep.getRotation().value(); + } + + @Override + public void onDraw(Canvas canvas) { + MasterImage master = MasterImage.getImage(); + Bitmap image = master.getFiltersOnlyImage(); + if (image == null) { + return; + } + GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep); + GeometryMathUtils.drawTransformedCropped(mDrawHolder, canvas, image, canvas.getWidth(), + canvas.getHeight()); + } + + public void setEditor(EditorRotate editorRotate) { + mEditorRotate = editorRotate; + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java new file mode 100644 index 000000000..6278b2ad4 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.GestureDetector.OnDoubleTapListener; +import android.view.GestureDetector.OnGestureListener; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.tools.SaveImage; + +import java.io.File; + +public class ImageShow extends View implements OnGestureListener, + ScaleGestureDetector.OnScaleGestureListener, + OnDoubleTapListener { + + private static final String LOGTAG = "ImageShow"; + private static final boolean ENABLE_ZOOMED_COMPARISON = false; + + protected Paint mPaint = new Paint(); + protected int mTextSize; + protected int mTextPadding; + + protected int mBackgroundColor; + + private GestureDetector mGestureDetector = null; + private ScaleGestureDetector mScaleGestureDetector = null; + + protected Rect mImageBounds = new Rect(); + private boolean mOriginalDisabled = false; + private boolean mTouchShowOriginal = false; + private long mTouchShowOriginalDate = 0; + private final long mTouchShowOriginalDelayMin = 200; // 200ms + private int mShowOriginalDirection = 0; + private static int UNVEIL_HORIZONTAL = 1; + private static int UNVEIL_VERTICAL = 2; + + private Point mTouchDown = new Point(); + private Point mTouch = new Point(); + private boolean mFinishedScalingOperation = false; + + private int mOriginalTextMargin; + private int mOriginalTextSize; + private String mOriginalText; + private boolean mZoomIn = false; + Point mOriginalTranslation = new Point(); + float mOriginalScale; + float mStartFocusX, mStartFocusY; + private enum InteractionMode { + NONE, + SCALE, + MOVE + } + InteractionMode mInteractionMode = InteractionMode.NONE; + + private FilterShowActivity mActivity = null; + + public FilterShowActivity getActivity() { + return mActivity; + } + + public boolean hasModifications() { + return MasterImage.getImage().hasModifications(); + } + + public void resetParameter() { + // TODO: implement reset + } + + public void onNewValue(int parameter) { + invalidate(); + } + + public ImageShow(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setupImageShow(context); + } + + public ImageShow(Context context, AttributeSet attrs) { + super(context, attrs); + setupImageShow(context); + + } + + public ImageShow(Context context) { + super(context); + setupImageShow(context); + } + + private void setupImageShow(Context context) { + Resources res = context.getResources(); + mTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_text_size); + mTextPadding = res.getDimensionPixelSize(R.dimen.photoeditor_text_padding); + mOriginalTextMargin = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_margin); + mOriginalTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_size); + mBackgroundColor = res.getColor(R.color.background_screen); + mOriginalText = res.getString(R.string.original_picture_text); + setupGestureDetector(context); + mActivity = (FilterShowActivity) context; + MasterImage.getImage().addObserver(this); + } + + public void setupGestureDetector(Context context) { + mGestureDetector = new GestureDetector(context, this); + mScaleGestureDetector = new ScaleGestureDetector(context, this); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(parentWidth, parentHeight); + } + + public ImageFilter getCurrentFilter() { + return MasterImage.getImage().getCurrentFilter(); + } + + /* consider moving the following 2 methods into a subclass */ + /** + * This function calculates a Image to Screen Transformation matrix + * + * @param reflectRotation set true if you want the rotation encoded + * @return Image to Screen transformation matrix + */ + protected Matrix getImageToScreenMatrix(boolean reflectRotation) { + MasterImage master = MasterImage.getImage(); + if (master.getOriginalBounds() == null) { + return new Matrix(); + } + Matrix m = GeometryMathUtils.getImageToScreenMatrix(master.getPreset().getGeometryFilters(), + reflectRotation, master.getOriginalBounds(), getWidth(), getHeight()); + Point translate = master.getTranslation(); + float scaleFactor = master.getScaleFactor(); + m.postTranslate(translate.x, translate.y); + m.postScale(scaleFactor, scaleFactor, getWidth() / 2.0f, getHeight() / 2.0f); + return m; + } + + /** + * This function calculates a to Screen Image Transformation matrix + * + * @param reflectRotation set true if you want the rotation encoded + * @return Screen to Image transformation matrix + */ + protected Matrix getScreenToImageMatrix(boolean reflectRotation) { + Matrix m = getImageToScreenMatrix(reflectRotation); + Matrix invert = new Matrix(); + m.invert(invert); + return invert; + } + + public ImagePreset getImagePreset() { + return MasterImage.getImage().getPreset(); + } + + @Override + public void onDraw(Canvas canvas) { + MasterImage.getImage().setImageShowSize(getWidth(), getHeight()); + + float cx = canvas.getWidth()/2.0f; + float cy = canvas.getHeight()/2.0f; + float scaleFactor = MasterImage.getImage().getScaleFactor(); + Point translation = MasterImage.getImage().getTranslation(); + + Matrix scalingMatrix = new Matrix(); + scalingMatrix.postScale(scaleFactor, scaleFactor, cx, cy); + scalingMatrix.preTranslate(translation.x, translation.y); + + RectF unscaledClipRect = new RectF(mImageBounds); + scalingMatrix.mapRect(unscaledClipRect, unscaledClipRect); + + canvas.save(); + + boolean enablePartialRendering = false; + + // For now, partial rendering is disabled for all filters, + // so no need to clip. + if (enablePartialRendering && !unscaledClipRect.isEmpty()) { + canvas.clipRect(unscaledClipRect); + } + + canvas.save(); + // TODO: center scale on gesture + canvas.scale(scaleFactor, scaleFactor, cx, cy); + canvas.translate(translation.x, translation.y); + drawImage(canvas, getFilteredImage(), true); + Bitmap highresPreview = MasterImage.getImage().getHighresImage(); + if (highresPreview != null) { + drawImage(canvas, highresPreview, true); + } + canvas.restore(); + + Bitmap partialPreview = MasterImage.getImage().getPartialImage(); + if (partialPreview != null) { + Rect src = new Rect(0, 0, partialPreview.getWidth(), partialPreview.getHeight()); + Rect dest = new Rect(0, 0, getWidth(), getHeight()); + canvas.drawBitmap(partialPreview, src, dest, mPaint); + } + + canvas.save(); + canvas.scale(scaleFactor, scaleFactor, cx, cy); + canvas.translate(translation.x, translation.y); + drawPartialImage(canvas, getGeometryOnlyImage()); + canvas.restore(); + + canvas.restore(); + } + + public void resetImageCaches(ImageShow caller) { + MasterImage.getImage().updatePresets(true); + } + + public Bitmap getFiltersOnlyImage() { + return MasterImage.getImage().getFiltersOnlyImage(); + } + + public Bitmap getGeometryOnlyImage() { + return MasterImage.getImage().getGeometryOnlyImage(); + } + + public Bitmap getFilteredImage() { + return MasterImage.getImage().getFilteredImage(); + } + + public void drawImage(Canvas canvas, Bitmap image, boolean updateBounds) { + if (image != null) { + Rect s = new Rect(0, 0, image.getWidth(), + image.getHeight()); + + float scale = GeometryMathUtils.scale(image.getWidth(), image.getHeight(), getWidth(), + getHeight()); + + float w = image.getWidth() * scale; + float h = image.getHeight() * scale; + float ty = (getHeight() - h) / 2.0f; + float tx = (getWidth() - w) / 2.0f; + + Rect d = new Rect((int) tx, (int) ty, (int) (w + tx), + (int) (h + ty)); + if (updateBounds) { + mImageBounds = d; + } + canvas.drawBitmap(image, s, d, mPaint); + } + } + + public void drawPartialImage(Canvas canvas, Bitmap image) { + boolean showsOriginal = MasterImage.getImage().showsOriginal(); + if (!showsOriginal && !mTouchShowOriginal) + return; + canvas.save(); + if (image != null) { + if (mShowOriginalDirection == 0) { + if (Math.abs(mTouch.y - mTouchDown.y) > Math.abs(mTouch.x - mTouchDown.x)) { + mShowOriginalDirection = UNVEIL_VERTICAL; + } else { + mShowOriginalDirection = UNVEIL_HORIZONTAL; + } + } + + int px = 0; + int py = 0; + if (mShowOriginalDirection == UNVEIL_VERTICAL) { + px = mImageBounds.width(); + py = mTouch.y - mImageBounds.top; + } else { + px = mTouch.x - mImageBounds.left; + py = mImageBounds.height(); + if (showsOriginal) { + px = mImageBounds.width(); + } + } + + Rect d = new Rect(mImageBounds.left, mImageBounds.top, + mImageBounds.left + px, mImageBounds.top + py); + canvas.clipRect(d); + drawImage(canvas, image, false); + Paint paint = new Paint(); + paint.setColor(Color.BLACK); + paint.setStrokeWidth(3); + + if (mShowOriginalDirection == UNVEIL_VERTICAL) { + canvas.drawLine(mImageBounds.left, mTouch.y, + mImageBounds.right, mTouch.y, paint); + } else { + canvas.drawLine(mTouch.x, mImageBounds.top, + mTouch.x, mImageBounds.bottom, paint); + } + + Rect bounds = new Rect(); + paint.setAntiAlias(true); + paint.setTextSize(mOriginalTextSize); + paint.getTextBounds(mOriginalText, 0, mOriginalText.length(), bounds); + paint.setColor(Color.BLACK); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin, + mImageBounds.top + bounds.height() + mOriginalTextMargin, paint); + paint.setStyle(Paint.Style.FILL); + paint.setStrokeWidth(1); + paint.setColor(Color.WHITE); + canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin, + mImageBounds.top + bounds.height() + mOriginalTextMargin, paint); + } + canvas.restore(); + } + + public void bindAsImageLoadListener() { + MasterImage.getImage().addListener(this); + } + + public void updateImage() { + invalidate(); + } + + public void imageLoaded() { + updateImage(); + } + + public void saveImage(FilterShowActivity filterShowActivity, File file) { + SaveImage.saveImage(getImagePreset(), filterShowActivity, file); + } + + + public boolean scaleInProgress() { + return mScaleGestureDetector.isInProgress(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + int action = event.getAction(); + action = action & MotionEvent.ACTION_MASK; + + mGestureDetector.onTouchEvent(event); + boolean scaleInProgress = scaleInProgress(); + mScaleGestureDetector.onTouchEvent(event); + if (mInteractionMode == InteractionMode.SCALE) { + return true; + } + if (!scaleInProgress() && scaleInProgress) { + // If we were scaling, the scale will stop but we will + // still issue an ACTION_UP. Let the subclasses know. + mFinishedScalingOperation = true; + } + + int ex = (int) event.getX(); + int ey = (int) event.getY(); + if (action == MotionEvent.ACTION_DOWN) { + mInteractionMode = InteractionMode.MOVE; + mTouchDown.x = ex; + mTouchDown.y = ey; + mTouchShowOriginalDate = System.currentTimeMillis(); + mShowOriginalDirection = 0; + MasterImage.getImage().setOriginalTranslation(MasterImage.getImage().getTranslation()); + } + + if (action == MotionEvent.ACTION_MOVE && mInteractionMode == InteractionMode.MOVE) { + mTouch.x = ex; + mTouch.y = ey; + + float scaleFactor = MasterImage.getImage().getScaleFactor(); + if (scaleFactor > 1 && (!ENABLE_ZOOMED_COMPARISON || event.getPointerCount() == 2)) { + float translateX = (mTouch.x - mTouchDown.x) / scaleFactor; + float translateY = (mTouch.y - mTouchDown.y) / scaleFactor; + Point originalTranslation = MasterImage.getImage().getOriginalTranslation(); + Point translation = MasterImage.getImage().getTranslation(); + translation.x = (int) (originalTranslation.x + translateX); + translation.y = (int) (originalTranslation.y + translateY); + constrainTranslation(translation, scaleFactor); + MasterImage.getImage().setTranslation(translation); + mTouchShowOriginal = false; + } else if (enableComparison() && !mOriginalDisabled + && (System.currentTimeMillis() - mTouchShowOriginalDate + > mTouchShowOriginalDelayMin) + && event.getPointerCount() == 1) { + mTouchShowOriginal = true; + } + } + + if (action == MotionEvent.ACTION_UP) { + mInteractionMode = InteractionMode.NONE; + mTouchShowOriginal = false; + mTouchDown.x = 0; + mTouchDown.y = 0; + mTouch.x = 0; + mTouch.y = 0; + if (MasterImage.getImage().getScaleFactor() <= 1) { + MasterImage.getImage().setScaleFactor(1); + MasterImage.getImage().resetTranslation(); + } + } + invalidate(); + return true; + } + + protected boolean enableComparison() { + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent arg0) { + mZoomIn = !mZoomIn; + float scale = 1.0f; + if (mZoomIn) { + scale = MasterImage.getImage().getMaxScaleFactor(); + } + if (scale != MasterImage.getImage().getScaleFactor()) { + MasterImage.getImage().setScaleFactor(scale); + float translateX = (getWidth() / 2 - arg0.getX()); + float translateY = (getHeight() / 2 - arg0.getY()); + Point translation = MasterImage.getImage().getTranslation(); + translation.x = (int) (mOriginalTranslation.x + translateX); + translation.y = (int) (mOriginalTranslation.y + translateY); + constrainTranslation(translation, scale); + MasterImage.getImage().setTranslation(translation); + invalidate(); + } + return true; + } + + private void constrainTranslation(Point translation, float scale) { + float maxTranslationX = getWidth() / scale; + float maxTranslationY = getHeight() / scale; + if (Math.abs(translation.x) > maxTranslationX) { + translation.x = (int) (Math.signum(translation.x) * + maxTranslationX); + if (Math.abs(translation.y) > maxTranslationY) { + translation.y = (int) (Math.signum(translation.y) * + maxTranslationY); + } + + } + } + + @Override + public boolean onDoubleTapEvent(MotionEvent arg0) { + return false; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent arg0) { + return false; + } + + @Override + public boolean onDown(MotionEvent arg0) { + return false; + } + + @Override + public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float arg2, float arg3) { + if (mActivity == null) { + return false; + } + if (endEvent.getPointerCount() == 2) { + return false; + } + return true; + } + + @Override + public void onLongPress(MotionEvent arg0) { + } + + @Override + public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) { + return false; + } + + @Override + public void onShowPress(MotionEvent arg0) { + } + + @Override + public boolean onSingleTapUp(MotionEvent arg0) { + return false; + } + + public boolean useUtilityPanel() { + return false; + } + + public void openUtilityPanel(final LinearLayout accessoryViewList) { + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + MasterImage img = MasterImage.getImage(); + float scaleFactor = img.getScaleFactor(); + + scaleFactor = scaleFactor * detector.getScaleFactor(); + if (scaleFactor > MasterImage.getImage().getMaxScaleFactor()) { + scaleFactor = MasterImage.getImage().getMaxScaleFactor(); + } + if (scaleFactor < 0.5) { + scaleFactor = 0.5f; + } + MasterImage.getImage().setScaleFactor(scaleFactor); + scaleFactor = img.getScaleFactor(); + float focusx = detector.getFocusX(); + float focusy = detector.getFocusY(); + float translateX = (focusx - mStartFocusX) / scaleFactor; + float translateY = (focusy - mStartFocusY) / scaleFactor; + Point translation = MasterImage.getImage().getTranslation(); + translation.x = (int) (mOriginalTranslation.x + translateX); + translation.y = (int) (mOriginalTranslation.y + translateY); + constrainTranslation(translation, scaleFactor); + MasterImage.getImage().setTranslation(translation); + + invalidate(); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + Point pos = MasterImage.getImage().getTranslation(); + mOriginalTranslation.x = pos.x; + mOriginalTranslation.y = pos.y; + mOriginalScale = MasterImage.getImage().getScaleFactor(); + mStartFocusX = detector.getFocusX(); + mStartFocusY = detector.getFocusY(); + mInteractionMode = InteractionMode.SCALE; + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mInteractionMode = InteractionMode.NONE; + if (MasterImage.getImage().getScaleFactor() < 1) { + MasterImage.getImage().setScaleFactor(1); + invalidate(); + } + } + + public boolean didFinishScalingOperation() { + if (mFinishedScalingOperation) { + mFinishedScalingOperation = false; + return true; + } + return false; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java new file mode 100644 index 000000000..ff75dcc09 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorStraighten; +import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation; +import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; + +import java.util.ArrayList; +import java.util.Collection; + + +public class ImageStraighten extends ImageShow { + private static final String TAG = ImageStraighten.class.getSimpleName(); + private float mBaseAngle = 0; + private float mAngle = 0; + private float mInitialAngle = 0; + private boolean mFirstDrawSinceUp = false; + private EditorStraighten mEditorStraighten; + private FilterStraightenRepresentation mLocalRep = new FilterStraightenRepresentation(); + private RectF mPriorCropAtUp = new RectF(); + private RectF mDrawRect = new RectF(); + private Path mDrawPath = new Path(); + private GeometryHolder mDrawHolder = new GeometryHolder(); + private enum MODES { + NONE, MOVE + } + private MODES mState = MODES.NONE; + private static final float MAX_STRAIGHTEN_ANGLE + = FilterStraightenRepresentation.MAX_STRAIGHTEN_ANGLE; + private static final float MIN_STRAIGHTEN_ANGLE + = FilterStraightenRepresentation.MIN_STRAIGHTEN_ANGLE; + private float mCurrentX; + private float mCurrentY; + private float mTouchCenterX; + private float mTouchCenterY; + private RectF mCrop = new RectF(); + private final Paint mPaint = new Paint(); + + public ImageStraighten(Context context) { + super(context); + } + + public ImageStraighten(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setFilterStraightenRepresentation(FilterStraightenRepresentation rep) { + mLocalRep = (rep == null) ? new FilterStraightenRepresentation() : rep; + mInitialAngle = mBaseAngle = mAngle = mLocalRep.getStraighten(); + } + + public Collection<FilterRepresentation> getFinalRepresentation() { + ArrayList<FilterRepresentation> reps = new ArrayList<FilterRepresentation>(2); + reps.add(mLocalRep); + if (mInitialAngle != mLocalRep.getStraighten()) { + reps.add(new FilterCropRepresentation(mCrop)); + } + return reps; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + + switch (event.getActionMasked()) { + case (MotionEvent.ACTION_DOWN): + if (mState == MODES.NONE) { + mTouchCenterX = x; + mTouchCenterY = y; + mCurrentX = x; + mCurrentY = y; + mState = MODES.MOVE; + mBaseAngle = mAngle; + } + break; + case (MotionEvent.ACTION_UP): + if (mState == MODES.MOVE) { + mState = MODES.NONE; + mCurrentX = x; + mCurrentY = y; + computeValue(); + mFirstDrawSinceUp = true; + } + break; + case (MotionEvent.ACTION_MOVE): + if (mState == MODES.MOVE) { + mCurrentX = x; + mCurrentY = y; + computeValue(); + } + break; + default: + break; + } + invalidate(); + return true; + } + + private static float angleFor(float dx, float dy) { + return (float) (Math.atan2(dx, dy) * 180 / Math.PI); + } + + private float getCurrentTouchAngle() { + float centerX = getWidth() / 2f; + float centerY = getHeight() / 2f; + if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) { + return 0; + } + float dX1 = mTouchCenterX - centerX; + float dY1 = mTouchCenterY - centerY; + float dX2 = mCurrentX - centerX; + float dY2 = mCurrentY - centerY; + float angleA = angleFor(dX1, dY1); + float angleB = angleFor(dX2, dY2); + return (angleB - angleA) % 360; + } + + private void computeValue() { + float angle = getCurrentTouchAngle(); + mAngle = (mBaseAngle - angle) % 360; + mAngle = Math.max(MIN_STRAIGHTEN_ANGLE, mAngle); + mAngle = Math.min(MAX_STRAIGHTEN_ANGLE, mAngle); + } + + private static void getUntranslatedStraightenCropBounds(RectF outRect, float straightenAngle) { + float deg = straightenAngle; + if (deg < 0) { + deg = -deg; + } + double a = Math.toRadians(deg); + double sina = Math.sin(a); + double cosa = Math.cos(a); + double rw = outRect.width(); + double rh = outRect.height(); + double h1 = rh * rh / (rw * sina + rh * cosa); + double h2 = rh * rw / (rw * cosa + rh * sina); + double hh = Math.min(h1, h2); + double ww = hh * rw / rh; + float left = (float) ((rw - ww) * 0.5f); + float top = (float) ((rh - hh) * 0.5f); + float right = (float) (left + ww); + float bottom = (float) (top + hh); + outRect.set(left, top, right, bottom); + } + + private void updateCurrentCrop(Matrix m, GeometryHolder h, RectF tmp, int imageWidth, + int imageHeight, int viewWidth, int viewHeight) { + if (GeometryMathUtils.needsDimensionSwap(h.rotation)) { + tmp.set(0, 0, imageHeight, imageWidth); + } else { + tmp.set(0, 0, imageWidth, imageHeight); + } + float scale = GeometryMathUtils.scale(imageWidth, imageHeight, viewWidth, viewHeight); + GeometryMathUtils.scaleRect(tmp, scale); + getUntranslatedStraightenCropBounds(tmp, mAngle); + tmp.offset(viewWidth / 2f - tmp.centerX(), viewHeight / 2f - tmp.centerY()); + h.straighten = 0; + Matrix m1 = GeometryMathUtils.getFullGeometryToScreenMatrix(h, imageWidth, + imageHeight, viewWidth, viewHeight); + m.reset(); + m1.invert(m); + mCrop.set(tmp); + m.mapRect(mCrop); + FilterCropRepresentation.findNormalizedCrop(mCrop, imageWidth, imageHeight); + } + + + @Override + public void onDraw(Canvas canvas) { + MasterImage master = MasterImage.getImage(); + Bitmap image = master.getFiltersOnlyImage(); + if (image == null) { + return; + } + GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep); + mDrawHolder.straighten = mAngle; + int imageWidth = image.getWidth(); + int imageHeight = image.getHeight(); + int viewWidth = canvas.getWidth(); + int viewHeight = canvas.getHeight(); + + // Get matrix for drawing bitmap + Matrix m = GeometryMathUtils.getFullGeometryToScreenMatrix(mDrawHolder, imageWidth, + imageHeight, viewWidth, viewHeight); + mPaint.reset(); + mPaint.setAntiAlias(true); + mPaint.setFilterBitmap(true); + canvas.drawBitmap(image, m, mPaint); + + mPaint.setFilterBitmap(false); + mPaint.setColor(Color.WHITE); + mPaint.setStrokeWidth(2); + mPaint.setStyle(Paint.Style.FILL_AND_STROKE); + updateCurrentCrop(m, mDrawHolder, mDrawRect, imageWidth, + imageHeight, viewWidth, viewHeight); + if (mFirstDrawSinceUp) { + mPriorCropAtUp.set(mCrop); + mLocalRep.setStraighten(mAngle); + mFirstDrawSinceUp = false; + } + + // Draw the grid + if (mState == MODES.MOVE) { + canvas.save(); + canvas.clipRect(mDrawRect); + int n = 16; + float step = viewWidth / n; + float p = 0; + for (int i = 1; i < n; i++) { + p = i * step; + mPaint.setAlpha(60); + canvas.drawLine(p, 0, p, viewHeight, mPaint); + canvas.drawLine(0, p, viewHeight, p, mPaint); + } + canvas.restore(); + } + mPaint.reset(); + mPaint.setColor(Color.WHITE); + mPaint.setStyle(Style.STROKE); + mPaint.setStrokeWidth(3); + mDrawPath.reset(); + mDrawPath.addRect(mDrawRect, Path.Direction.CW); + canvas.drawPath(mDrawPath, mPaint); + } + + public void setEditor(EditorStraighten editorStraighten) { + mEditorStraighten = editorStraighten; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java new file mode 100644 index 000000000..25a0a9073 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.ScaleGestureDetector.OnScaleGestureListener; + +import com.android.gallery3d.filtershow.editors.BasicEditor; +import com.android.gallery3d.filtershow.editors.EditorTinyPlanet; +import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation; + +public class ImageTinyPlanet extends ImageShow { + private static final String LOGTAG = "ImageTinyPlanet"; + + private float mTouchCenterX = 0; + private float mTouchCenterY = 0; + private float mCurrentX = 0; + private float mCurrentY = 0; + private float mCenterX = 0; + private float mCenterY = 0; + private float mStartAngle = 0; + private FilterTinyPlanetRepresentation mTinyPlanetRep; + private EditorTinyPlanet mEditorTinyPlanet; + private ScaleGestureDetector mScaleGestureDetector = null; + boolean mInScale = false; + RectF mDestRect = new RectF(); + + OnScaleGestureListener mScaleGestureListener = new OnScaleGestureListener() { + private float mScale = 100; + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mInScale = false; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + mInScale = true; + mScale = mTinyPlanetRep.getValue(); + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + int value = mTinyPlanetRep.getValue(); + mScale *= detector.getScaleFactor(); + value = (int) (mScale); + value = Math.min(mTinyPlanetRep.getMaximum(), value); + value = Math.max(mTinyPlanetRep.getMinimum(), value); + mTinyPlanetRep.setValue(value); + invalidate(); + mEditorTinyPlanet.commitLocalRepresentation(); + mEditorTinyPlanet.updateUI(); + return true; + } + }; + + public ImageTinyPlanet(Context context) { + super(context); + mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener); + } + + public ImageTinyPlanet(Context context, AttributeSet attrs) { + super(context, attrs); + mScaleGestureDetector = new ScaleGestureDetector(context,mScaleGestureListener ); + } + + protected static float angleFor(float dx, float dy) { + return (float) (Math.atan2(dx, dy) * 180 / Math.PI); + } + + protected float getCurrentTouchAngle() { + if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) { + return 0; + } + float dX1 = mTouchCenterX - mCenterX; + float dY1 = mTouchCenterY - mCenterY; + float dX2 = mCurrentX - mCenterX; + float dY2 = mCurrentY - mCenterY; + + float angleA = angleFor(dX1, dY1); + float angleB = angleFor(dX2, dY2); + return (float) (((angleB - angleA) % 360) * Math.PI / 180); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + mCurrentX = x; + mCurrentY = y; + mCenterX = getWidth() / 2; + mCenterY = getHeight() / 2; + mScaleGestureDetector.onTouchEvent(event); + if (mInScale) { + return true; + } + switch (event.getActionMasked()) { + case (MotionEvent.ACTION_DOWN): + mTouchCenterX = x; + mTouchCenterY = y; + mStartAngle = mTinyPlanetRep.getAngle(); + break; + + case (MotionEvent.ACTION_MOVE): + mTinyPlanetRep.setAngle(mStartAngle + getCurrentTouchAngle()); + break; + } + invalidate(); + mEditorTinyPlanet.commitLocalRepresentation(); + return true; + } + + public void setRepresentation(FilterTinyPlanetRepresentation tinyPlanetRep) { + mTinyPlanetRep = tinyPlanetRep; + } + + public void setEditor(BasicEditor editorTinyPlanet) { + mEditorTinyPlanet = (EditorTinyPlanet) editorTinyPlanet; + } + + @Override + public void onDraw(Canvas canvas) { + Bitmap bitmap = MasterImage.getImage().getHighresImage(); + if (bitmap == null) { + bitmap = MasterImage.getImage().getFilteredImage(); + } + + if (bitmap != null) { + display(canvas, bitmap); + } + } + + private void display(Canvas canvas, Bitmap bitmap) { + float sw = canvas.getWidth(); + float sh = canvas.getHeight(); + float iw = bitmap.getWidth(); + float ih = bitmap.getHeight(); + float nsw = sw; + float nsh = sh; + + if (sw * ih > sh * iw) { + nsw = sh * iw / ih; + } else { + nsh = sw * ih / iw; + } + + mDestRect.left = (sw - nsw) / 2; + mDestRect.top = (sh - nsh) / 2; + mDestRect.right = sw - mDestRect.left; + mDestRect.bottom = sh - mDestRect.top; + + canvas.drawBitmap(bitmap, null, mDestRect, mPaint); + } +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java new file mode 100644 index 000000000..518969ee1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.gallery3d.filtershow.editors.EditorVignette; +import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation; + +public class ImageVignette extends ImageShow { + private static final String LOGTAG = "ImageVignette"; + + private FilterVignetteRepresentation mVignetteRep; + private EditorVignette mEditorVignette; + + private int mActiveHandle = -1; + + EclipseControl mElipse; + + public ImageVignette(Context context) { + super(context); + mElipse = new EclipseControl(context); + } + + public ImageVignette(Context context, AttributeSet attrs) { + super(context, attrs); + mElipse = new EclipseControl(context); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int mask = event.getActionMasked(); + if (mActiveHandle == -1) { + if (MotionEvent.ACTION_DOWN != mask) { + return super.onTouchEvent(event); + } + if (event.getPointerCount() == 1) { + mActiveHandle = mElipse.getCloseHandle(event.getX(), event.getY()); + } + if (mActiveHandle == -1) { + return super.onTouchEvent(event); + } + } else { + switch (mask) { + case MotionEvent.ACTION_UP: + mActiveHandle = -1; + break; + case MotionEvent.ACTION_DOWN: + break; + } + } + float x = event.getX(); + float y = event.getY(); + + mElipse.setScrToImageMatrix(getScreenToImageMatrix(true)); + + boolean didComputeEllipses = false; + switch (mask) { + case (MotionEvent.ACTION_DOWN): + mElipse.actionDown(x, y, mVignetteRep); + break; + case (MotionEvent.ACTION_UP): + case (MotionEvent.ACTION_MOVE): + mElipse.actionMove(mActiveHandle, x, y, mVignetteRep); + setRepresentation(mVignetteRep); + didComputeEllipses = true; + break; + } + if (!didComputeEllipses) { + computeEllipses(); + } + invalidate(); + return true; + } + + public void setRepresentation(FilterVignetteRepresentation vignetteRep) { + mVignetteRep = vignetteRep; + computeEllipses(); + } + + public void computeEllipses() { + if (mVignetteRep == null) { + return; + } + Matrix toImg = getScreenToImageMatrix(false); + Matrix toScr = new Matrix(); + toImg.invert(toScr); + + float[] c = new float[] { + mVignetteRep.getCenterX(), mVignetteRep.getCenterY() }; + if (Float.isNaN(c[0])) { + float cx = MasterImage.getImage().getOriginalBounds().width() / 2; + float cy = MasterImage.getImage().getOriginalBounds().height() / 2; + float rx = Math.min(cx, cy) * .8f; + float ry = rx; + mVignetteRep.setCenter(cx, cy); + mVignetteRep.setRadius(rx, ry); + + c[0] = cx; + c[1] = cy; + toScr.mapPoints(c); + if (getWidth() != 0) { + mElipse.setCenter(c[0], c[1]); + mElipse.setRadius(c[0] * 0.8f, c[1] * 0.8f); + } + } else { + + toScr.mapPoints(c); + + mElipse.setCenter(c[0], c[1]); + mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()), + toScr.mapRadius(mVignetteRep.getRadiusY())); + } + mEditorVignette.commitLocalRepresentation(); + } + + public void setEditor(EditorVignette editorVignette) { + mEditorVignette = editorVignette; + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + computeEllipses(); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mVignetteRep == null) { + return; + } + Matrix toImg = getScreenToImageMatrix(false); + Matrix toScr = new Matrix(); + toImg.invert(toScr); + float[] c = new float[] { + mVignetteRep.getCenterX(), mVignetteRep.getCenterY() }; + toScr.mapPoints(c); + mElipse.setCenter(c[0], c[1]); + mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()), + toScr.mapRadius(mVignetteRep.getRadiusY())); + + mElipse.draw(canvas); + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/Line.java b/src/com/android/gallery3d/filtershow/imageshow/Line.java new file mode 100644 index 000000000..a767bd809 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/Line.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +public interface Line { + void setPoint1(float x, float y); + void setPoint2(float x, float y); + float getPoint1X(); + float getPoint1Y(); + float getPoint2X(); + float getPoint2Y(); +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java new file mode 100644 index 000000000..92e57bfc1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java @@ -0,0 +1,581 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; + +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FilterRepresentation; +import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.history.HistoryItem; +import com.android.gallery3d.filtershow.history.HistoryManager; +import com.android.gallery3d.filtershow.pipeline.Buffer; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.pipeline.RenderingRequest; +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; +import com.android.gallery3d.filtershow.pipeline.SharedBuffer; +import com.android.gallery3d.filtershow.pipeline.SharedPreset; +import com.android.gallery3d.filtershow.state.StateAdapter; + +import java.util.Vector; + +public class MasterImage implements RenderingRequestCaller { + + private static final String LOGTAG = "MasterImage"; + private boolean DEBUG = false; + private static final boolean DISABLEZOOM = false; + public static final int SMALL_BITMAP_DIM = 160; + public static final int MAX_BITMAP_DIM = 900; + private static MasterImage sMasterImage = null; + + private boolean mSupportsHighRes = false; + + private ImageFilter mCurrentFilter = null; + private ImagePreset mPreset = null; + private ImagePreset mLoadedPreset = null; + private ImagePreset mGeometryOnlyPreset = null; + private ImagePreset mFiltersOnlyPreset = null; + + private SharedBuffer mPreviewBuffer = new SharedBuffer(); + private SharedPreset mPreviewPreset = new SharedPreset(); + + private Bitmap mOriginalBitmapSmall = null; + private Bitmap mOriginalBitmapLarge = null; + private Bitmap mOriginalBitmapHighres = null; + private int mOrientation; + private Rect mOriginalBounds; + private final Vector<ImageShow> mLoadListeners = new Vector<ImageShow>(); + private Uri mUri = null; + private int mZoomOrientation = ImageLoader.ORI_NORMAL; + + private Bitmap mGeometryOnlyBitmap = null; + private Bitmap mFiltersOnlyBitmap = null; + private Bitmap mPartialBitmap = null; + private Bitmap mHighresBitmap = null; + + private HistoryManager mHistory = null; + private StateAdapter mState = null; + + private FilterShowActivity mActivity = null; + + private Vector<ImageShow> mObservers = new Vector<ImageShow>(); + private FilterRepresentation mCurrentFilterRepresentation; + + private float mScaleFactor = 1.0f; + private float mMaxScaleFactor = 3.0f; // TODO: base this on the current view / image + private Point mTranslation = new Point(); + private Point mOriginalTranslation = new Point(); + + private Point mImageShowSize = new Point(); + + private boolean mShowsOriginal; + + private MasterImage() { + } + + // TODO: remove singleton + public static void setMaster(MasterImage master) { + sMasterImage = master; + } + + public static MasterImage getImage() { + if (sMasterImage == null) { + sMasterImage = new MasterImage(); + } + return sMasterImage; + } + + public Bitmap getOriginalBitmapSmall() { + return mOriginalBitmapSmall; + } + + public Bitmap getOriginalBitmapLarge() { + return mOriginalBitmapLarge; + } + + public Bitmap getOriginalBitmapHighres() { + return mOriginalBitmapHighres; + } + + public void setOriginalBitmapHighres(Bitmap mOriginalBitmapHighres) { + this.mOriginalBitmapHighres = mOriginalBitmapHighres; + } + + public int getOrientation() { + return mOrientation; + } + + public Rect getOriginalBounds() { + return mOriginalBounds; + } + + public void setOriginalBounds(Rect r) { + mOriginalBounds = r; + } + + public Uri getUri() { + return mUri; + } + + public void setUri(Uri uri) { + mUri = uri; + } + + public int getZoomOrientation() { + return mZoomOrientation; + } + + public void addListener(ImageShow imageShow) { + if (!mLoadListeners.contains(imageShow)) { + mLoadListeners.add(imageShow); + } + } + + public void warnListeners() { + mActivity.runOnUiThread(mWarnListenersRunnable); + } + + private Runnable mWarnListenersRunnable = new Runnable() { + @Override + public void run() { + for (int i = 0; i < mLoadListeners.size(); i++) { + ImageShow imageShow = mLoadListeners.elementAt(i); + imageShow.imageLoaded(); + } + invalidatePreview(); + } + }; + + public boolean loadBitmap(Uri uri, int size) { + setUri(uri); + mOrientation = ImageLoader.getMetadataOrientation(mActivity, uri); + Rect originalBounds = new Rect(); + mOriginalBitmapLarge = ImageLoader.loadOrientedConstrainedBitmap(uri, mActivity, + Math.min(MAX_BITMAP_DIM, size), + mOrientation, originalBounds); + setOriginalBounds(originalBounds); + if (mOriginalBitmapLarge == null) { + return false; + } + int sw = SMALL_BITMAP_DIM; + int sh = (int) (sw * (float) mOriginalBitmapLarge.getHeight() / mOriginalBitmapLarge + .getWidth()); + mOriginalBitmapSmall = Bitmap.createScaledBitmap(mOriginalBitmapLarge, sw, sh, true); + mZoomOrientation = mOrientation; + warnListeners(); + return true; + } + + public void setSupportsHighRes(boolean value) { + mSupportsHighRes = value; + } + + public void addObserver(ImageShow observer) { + if (mObservers.contains(observer)) { + return; + } + mObservers.add(observer); + } + + public void setActivity(FilterShowActivity activity) { + mActivity = activity; + } + + public FilterShowActivity getActivity() { + return mActivity; + } + + public synchronized ImagePreset getPreset() { + return mPreset; + } + + public synchronized ImagePreset getGeometryPreset() { + return mGeometryOnlyPreset; + } + + public synchronized ImagePreset getFiltersOnlyPreset() { + return mFiltersOnlyPreset; + } + + public synchronized void setPreset(ImagePreset preset, + FilterRepresentation change, + boolean addToHistory) { + if (DEBUG) { + preset.showFilters(); + } + mPreset = preset; + mPreset.fillImageStateAdapter(mState); + if (addToHistory) { + HistoryItem historyItem = new HistoryItem(mPreset, change); + mHistory.addHistoryItem(historyItem); + } + updatePresets(true); + mActivity.updateCategories(); + } + + public void onHistoryItemClick(int position) { + HistoryItem historyItem = mHistory.getItem(position); + // We need a copy from the history + ImagePreset newPreset = new ImagePreset(historyItem.getImagePreset()); + // don't need to add it to the history + setPreset(newPreset, historyItem.getFilterRepresentation(), false); + mHistory.setCurrentPreset(position); + } + + public HistoryManager getHistory() { + return mHistory; + } + + public StateAdapter getState() { + return mState; + } + + public void setHistoryManager(HistoryManager adapter) { + mHistory = adapter; + } + + public void setStateAdapter(StateAdapter adapter) { + mState = adapter; + } + + public void setCurrentFilter(ImageFilter filter) { + mCurrentFilter = filter; + } + + public ImageFilter getCurrentFilter() { + return mCurrentFilter; + } + + public synchronized boolean hasModifications() { + // TODO: We need to have a better same effects check to see if two + // presets are functionally the same. Right now, we are relying on a + // stricter check as equals(). + ImagePreset loadedPreset = getLoadedPreset(); + if (mPreset == null) { + if (loadedPreset == null) { + return false; + } else { + return loadedPreset.hasModifications(); + } + } else { + if (loadedPreset == null) { + return mPreset.hasModifications(); + } else { + return !mPreset.equals(loadedPreset); + } + } + } + + public SharedBuffer getPreviewBuffer() { + return mPreviewBuffer; + } + + public SharedPreset getPreviewPreset() { + return mPreviewPreset; + } + + public Bitmap getFilteredImage() { + mPreviewBuffer.swapConsumerIfNeeded(); // get latest bitmap + Buffer consumer = mPreviewBuffer.getConsumer(); + if (consumer != null) { + return consumer.getBitmap(); + } + return null; + } + + public Bitmap getFiltersOnlyImage() { + return mFiltersOnlyBitmap; + } + + public Bitmap getGeometryOnlyImage() { + return mGeometryOnlyBitmap; + } + + public Bitmap getPartialImage() { + return mPartialBitmap; + } + + public Bitmap getHighresImage() { + return mHighresBitmap; + } + + public void notifyObservers() { + for (ImageShow observer : mObservers) { + observer.invalidate(); + } + } + + public void updatePresets(boolean force) { + if (force || mGeometryOnlyPreset == null) { + ImagePreset newPreset = new ImagePreset(mPreset); + newPreset.setDoApplyFilters(false); + newPreset.setDoApplyGeometry(true); + if (force || mGeometryOnlyPreset == null + || !newPreset.same(mGeometryOnlyPreset)) { + mGeometryOnlyPreset = newPreset; + RenderingRequest.post(mActivity, getOriginalBitmapLarge(), + mGeometryOnlyPreset, RenderingRequest.GEOMETRY_RENDERING, this); + } + } + if (force || mFiltersOnlyPreset == null) { + ImagePreset newPreset = new ImagePreset(mPreset); + newPreset.setDoApplyFilters(true); + newPreset.setDoApplyGeometry(false); + if (force || mFiltersOnlyPreset == null + || !newPreset.same(mFiltersOnlyPreset)) { + mFiltersOnlyPreset = newPreset; + RenderingRequest.post(mActivity, MasterImage.getImage().getOriginalBitmapLarge(), + mFiltersOnlyPreset, RenderingRequest.FILTERS_RENDERING, this); + } + } + invalidatePreview(); + } + + public FilterRepresentation getCurrentFilterRepresentation() { + return mCurrentFilterRepresentation; + } + + public void setCurrentFilterRepresentation(FilterRepresentation currentFilterRepresentation) { + mCurrentFilterRepresentation = currentFilterRepresentation; + } + + public void invalidateFiltersOnly() { + mFiltersOnlyPreset = null; + updatePresets(false); + } + + public void invalidatePartialPreview() { + if (mPartialBitmap != null) { + mPartialBitmap = null; + notifyObservers(); + } + } + + public void invalidateHighresPreview() { + if (mHighresBitmap != null) { + mHighresBitmap = null; + notifyObservers(); + } + } + + public void invalidatePreview() { + mPreviewPreset.enqueuePreset(mPreset); + mPreviewBuffer.invalidate(); + invalidatePartialPreview(); + invalidateHighresPreview(); + needsUpdatePartialPreview(); + needsUpdateHighResPreview(); + mActivity.getProcessingService().updatePreviewBuffer(); + } + + public void setImageShowSize(int w, int h) { + if (mImageShowSize.x != w || mImageShowSize.y != h) { + mImageShowSize.set(w, h); + needsUpdatePartialPreview(); + needsUpdateHighResPreview(); + } + } + + private Matrix getImageToScreenMatrix(boolean reflectRotation) { + if (getOriginalBounds() == null || mImageShowSize.x == 0 || mImageShowSize.y == 0) { + return new Matrix(); + } + Matrix m = GeometryMathUtils.getImageToScreenMatrix(mPreset.getGeometryFilters(), + reflectRotation, getOriginalBounds(), mImageShowSize.x, mImageShowSize.y); + if (m == null) { + m = new Matrix(); + m.reset(); + return m; + } + Point translate = getTranslation(); + float scaleFactor = getScaleFactor(); + m.postTranslate(translate.x, translate.y); + m.postScale(scaleFactor, scaleFactor, mImageShowSize.x / 2.0f, mImageShowSize.y / 2.0f); + return m; + } + + private Matrix getScreenToImageMatrix(boolean reflectRotation) { + Matrix m = getImageToScreenMatrix(reflectRotation); + Matrix invert = new Matrix(); + m.invert(invert); + return invert; + } + + public void needsUpdateHighResPreview() { + if (!mSupportsHighRes) { + return; + } + if (mActivity.getProcessingService() == null) { + return; + } + mActivity.getProcessingService().postHighresRenderingRequest(mPreset, + getScaleFactor(), this); + invalidateHighresPreview(); + } + + public void needsUpdatePartialPreview() { + if (mPreset == null) { + return; + } + if (!mPreset.canDoPartialRendering()) { + invalidatePartialPreview(); + return; + } + Matrix m = getScreenToImageMatrix(true); + RectF r = new RectF(0, 0, mImageShowSize.x, mImageShowSize.y); + RectF dest = new RectF(); + m.mapRect(dest, r); + Rect bounds = new Rect(); + dest.roundOut(bounds); + RenderingRequest.post(mActivity, null, mPreset, RenderingRequest.PARTIAL_RENDERING, + this, bounds, new Rect(0, 0, mImageShowSize.x, mImageShowSize.y)); + invalidatePartialPreview(); + } + + @Override + public void available(RenderingRequest request) { + if (request.getBitmap() == null) { + return; + } + + boolean needsCheckModification = false; + if (request.getType() == RenderingRequest.GEOMETRY_RENDERING) { + mGeometryOnlyBitmap = request.getBitmap(); + needsCheckModification = true; + } + if (request.getType() == RenderingRequest.FILTERS_RENDERING) { + mFiltersOnlyBitmap = request.getBitmap(); + notifyObservers(); + needsCheckModification = true; + } + if (request.getType() == RenderingRequest.PARTIAL_RENDERING + && request.getScaleFactor() == getScaleFactor()) { + mPartialBitmap = request.getBitmap(); + notifyObservers(); + needsCheckModification = true; + } + if (request.getType() == RenderingRequest.HIGHRES_RENDERING) { + mHighresBitmap = request.getBitmap(); + notifyObservers(); + needsCheckModification = true; + } + if (needsCheckModification) { + mActivity.enableSave(hasModifications()); + } + } + + public static void reset() { + sMasterImage = null; + } + + public float getScaleFactor() { + return mScaleFactor; + } + + public void setScaleFactor(float scaleFactor) { + if (DISABLEZOOM) { + return; + } + if (scaleFactor == mScaleFactor) { + return; + } + mScaleFactor = scaleFactor; + invalidatePartialPreview(); + } + + public Point getTranslation() { + return mTranslation; + } + + public void setTranslation(Point translation) { + if (DISABLEZOOM) { + mTranslation.x = 0; + mTranslation.y = 0; + return; + } + mTranslation.x = translation.x; + mTranslation.y = translation.y; + needsUpdatePartialPreview(); + } + + public Point getOriginalTranslation() { + return mOriginalTranslation; + } + + public void setOriginalTranslation(Point originalTranslation) { + if (DISABLEZOOM) { + return; + } + mOriginalTranslation.x = originalTranslation.x; + mOriginalTranslation.y = originalTranslation.y; + } + + public void resetTranslation() { + mTranslation.x = 0; + mTranslation.y = 0; + needsUpdatePartialPreview(); + } + + public Bitmap getThumbnailBitmap() { + return getOriginalBitmapSmall(); + } + + public Bitmap getLargeThumbnailBitmap() { + return getOriginalBitmapLarge(); + } + + public float getMaxScaleFactor() { + if (DISABLEZOOM) { + return 1; + } + return mMaxScaleFactor; + } + + public void setMaxScaleFactor(float maxScaleFactor) { + mMaxScaleFactor = maxScaleFactor; + } + + public boolean supportsHighRes() { + return mSupportsHighRes; + } + + public void setShowsOriginal(boolean value) { + mShowsOriginal = value; + notifyObservers(); + } + + public boolean showsOriginal() { + return mShowsOriginal; + } + + public void setLoadedPreset(ImagePreset preset) { + mLoadedPreset = preset; + } + + public ImagePreset getLoadedPreset() { + return mLoadedPreset; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/Oval.java b/src/com/android/gallery3d/filtershow/imageshow/Oval.java new file mode 100644 index 000000000..28f278f1c --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/Oval.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +public interface Oval { + void setCenter(float x, float y); + void setRadius(float w, float h); + float getCenterX(); + float getCenterY(); + float getRadiusX(); + float getRadiusY(); + void setRadiusY(float y); + void setRadiusX(float x); + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/Spline.java b/src/com/android/gallery3d/filtershow/imageshow/Spline.java new file mode 100644 index 000000000..3c27a4d0f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/imageshow/Spline.java @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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 com.android.gallery3d.filtershow.imageshow; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import java.util.Collections; +import java.util.Vector; + +public class Spline { + private final Vector<ControlPoint> mPoints; + private static Drawable mCurveHandle; + private static int mCurveHandleSize; + private static int mCurveWidth; + + public static final int RGB = 0; + public static final int RED = 1; + public static final int GREEN = 2; + public static final int BLUE = 3; + private static final String LOGTAG = "Spline"; + + private final Paint gPaint = new Paint(); + private ControlPoint mCurrentControlPoint = null; + + public Spline() { + mPoints = new Vector<ControlPoint>(); + } + + public Spline(Spline spline) { + mPoints = new Vector<ControlPoint>(); + for (int i = 0; i < spline.mPoints.size(); i++) { + ControlPoint p = spline.mPoints.elementAt(i); + ControlPoint newPoint = new ControlPoint(p); + mPoints.add(newPoint); + if (spline.mCurrentControlPoint == p) { + mCurrentControlPoint = newPoint; + } + } + Collections.sort(mPoints); + } + + public static void setCurveHandle(Drawable drawable, int size) { + mCurveHandle = drawable; + mCurveHandleSize = size; + } + + public static void setCurveWidth(int width) { + mCurveWidth = width; + } + + public static int curveHandleSize() { + return mCurveHandleSize; + } + + public static int colorForCurve(int curveIndex) { + switch (curveIndex) { + case Spline.RED: + return Color.RED; + case GREEN: + return Color.GREEN; + case BLUE: + return Color.BLUE; + } + return Color.WHITE; + } + + public boolean sameValues(Spline other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + + if (getNbPoints() != other.getNbPoints()) { + return false; + } + + for (int i = 0; i < getNbPoints(); i++) { + ControlPoint p = mPoints.elementAt(i); + ControlPoint otherPoint = other.mPoints.elementAt(i); + if (!p.sameValues(otherPoint)) { + return false; + } + } + return true; + } + + private void didMovePoint(ControlPoint point) { + mCurrentControlPoint = point; + } + + public void movePoint(int pick, float x, float y) { + if (pick < 0 || pick > mPoints.size() - 1) { + return; + } + ControlPoint point = mPoints.elementAt(pick); + point.x = x; + point.y = y; + didMovePoint(point); + } + + public boolean isOriginal() { + if (this.getNbPoints() != 2) { + return false; + } + if (mPoints.elementAt(0).x != 0 || mPoints.elementAt(0).y != 1) { + return false; + } + if (mPoints.elementAt(1).x != 1 || mPoints.elementAt(1).y != 0) { + return false; + } + return true; + } + + public void reset() { + mPoints.clear(); + addPoint(0.0f, 1.0f); + addPoint(1.0f, 0.0f); + } + + private void drawHandles(Canvas canvas, Drawable indicator, float centerX, float centerY) { + int left = (int) centerX - mCurveHandleSize / 2; + int top = (int) centerY - mCurveHandleSize / 2; + indicator.setBounds(left, top, left + mCurveHandleSize, top + mCurveHandleSize); + indicator.draw(canvas); + } + + public float[] getAppliedCurve() { + float[] curve = new float[256]; + ControlPoint[] points = new ControlPoint[mPoints.size()]; + for (int i = 0; i < mPoints.size(); i++) { + ControlPoint p = mPoints.get(i); + points[i] = new ControlPoint(p.x, p.y); + } + double[] derivatives = solveSystem(points); + int start = 0; + int end = 256; + if (points[0].x != 0) { + start = (int) (points[0].x * 256); + } + if (points[points.length - 1].x != 1) { + end = (int) (points[points.length - 1].x * 256); + } + for (int i = 0; i < start; i++) { + curve[i] = 1.0f - points[0].y; + } + for (int i = end; i < 256; i++) { + curve[i] = 1.0f - points[points.length - 1].y; + } + for (int i = start; i < end; i++) { + ControlPoint cur = null; + ControlPoint next = null; + double x = i / 256.0; + int pivot = 0; + for (int j = 0; j < points.length - 1; j++) { + if (x >= points[j].x && x <= points[j + 1].x) { + pivot = j; + } + } + cur = points[pivot]; + next = points[pivot + 1]; + if (x <= next.x) { + double x1 = cur.x; + double x2 = next.x; + double y1 = cur.y; + double y2 = next.y; + + // Use the second derivatives to apply the cubic spline + // equation: + double delta = (x2 - x1); + double delta2 = delta * delta; + double b = (x - x1) / delta; + double a = 1 - b; + double ta = a * y1; + double tb = b * y2; + double tc = (a * a * a - a) * derivatives[pivot]; + double td = (b * b * b - b) * derivatives[pivot + 1]; + double y = ta + tb + (delta2 / 6) * (tc + td); + if (y > 1.0f) { + y = 1.0f; + } + if (y < 0) { + y = 0; + } + curve[i] = (float) (1.0f - y); + } else { + curve[i] = 1.0f - next.y; + } + } + return curve; + } + + private void drawGrid(Canvas canvas, float w, float h) { + // Grid + gPaint.setARGB(128, 150, 150, 150); + gPaint.setStrokeWidth(1); + + float stepH = h / 9; + float stepW = w / 9; + + // central diagonal + gPaint.setARGB(255, 100, 100, 100); + gPaint.setStrokeWidth(2); + canvas.drawLine(0, h, w, 0, gPaint); + + gPaint.setARGB(128, 200, 200, 200); + gPaint.setStrokeWidth(4); + stepH = h / 3; + stepW = w / 3; + for (int j = 1; j < 3; j++) { + canvas.drawLine(0, j * stepH, w, j * stepH, gPaint); + canvas.drawLine(j * stepW, 0, j * stepW, h, gPaint); + } + canvas.drawLine(0, 0, 0, h, gPaint); + canvas.drawLine(w, 0, w, h, gPaint); + canvas.drawLine(0, 0, w, 0, gPaint); + canvas.drawLine(0, h, w, h, gPaint); + } + + public void draw(Canvas canvas, int color, int canvasWidth, int canvasHeight, + boolean showHandles, boolean moving) { + float w = canvasWidth - mCurveHandleSize; + float h = canvasHeight - mCurveHandleSize; + float dx = mCurveHandleSize / 2; + float dy = mCurveHandleSize / 2; + + // The cubic spline equation is (from numerical recipes in C): + // y = a(y_i) + b(y_i+1) + c(y"_i) + d(y"_i+1) + // + // with c(y"_i) and d(y"_i+1): + // c(y"_i) = 1/6 (a^3 - a) delta^2 (y"_i) + // d(y"_i_+1) = 1/6 (b^3 - b) delta^2 (y"_i+1) + // + // and delta: + // delta = x_i+1 - x_i + // + // To find the second derivatives y", we can rearrange the equation as: + // A(y"_i-1) + B(y"_i) + C(y"_i+1) = D + // + // With the coefficients A, B, C, D: + // A = 1/6 (x_i - x_i-1) + // B = 1/3 (x_i+1 - x_i-1) + // C = 1/6 (x_i+1 - x_i) + // D = (y_i+1 - y_i)/(x_i+1 - x_i) - (y_i - y_i-1)/(x_i - x_i-1) + // + // We can now easily solve the equation to find the second derivatives: + ControlPoint[] points = new ControlPoint[mPoints.size()]; + for (int i = 0; i < mPoints.size(); i++) { + ControlPoint p = mPoints.get(i); + points[i] = new ControlPoint(p.x * w, p.y * h); + } + double[] derivatives = solveSystem(points); + + Path path = new Path(); + path.moveTo(0, points[0].y); + for (int i = 0; i < points.length - 1; i++) { + double x1 = points[i].x; + double x2 = points[i + 1].x; + double y1 = points[i].y; + double y2 = points[i + 1].y; + + for (double x = x1; x < x2; x += 20) { + // Use the second derivatives to apply the cubic spline + // equation: + double delta = (x2 - x1); + double delta2 = delta * delta; + double b = (x - x1) / delta; + double a = 1 - b; + double ta = a * y1; + double tb = b * y2; + double tc = (a * a * a - a) * derivatives[i]; + double td = (b * b * b - b) * derivatives[i + 1]; + double y = ta + tb + (delta2 / 6) * (tc + td); + if (y > h) { + y = h; + } + if (y < 0) { + y = 0; + } + path.lineTo((float) x, (float) y); + } + } + canvas.save(); + canvas.translate(dx, dy); + drawGrid(canvas, w, h); + ControlPoint lastPoint = points[points.length - 1]; + path.lineTo(lastPoint.x, lastPoint.y); + path.lineTo(w, lastPoint.y); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setFilterBitmap(true); + paint.setDither(true); + paint.setStyle(Paint.Style.STROKE); + int curveWidth = mCurveWidth; + if (showHandles) { + curveWidth *= 1.5; + } + paint.setStrokeWidth(curveWidth + 2); + paint.setColor(Color.BLACK); + canvas.drawPath(path, paint); + + if (moving && mCurrentControlPoint != null) { + float px = mCurrentControlPoint.x * w; + float py = mCurrentControlPoint.y * h; + paint.setStrokeWidth(3); + paint.setColor(Color.BLACK); + canvas.drawLine(px, py, px, h, paint); + canvas.drawLine(0, py, px, py, paint); + paint.setStrokeWidth(1); + paint.setColor(color); + canvas.drawLine(px, py, px, h, paint); + canvas.drawLine(0, py, px, py, paint); + } + + paint.setStrokeWidth(curveWidth); + paint.setColor(color); + canvas.drawPath(path, paint); + if (showHandles) { + for (int i = 0; i < points.length; i++) { + float x = points[i].x; + float y = points[i].y; + drawHandles(canvas, mCurveHandle, x, y); + } + } + canvas.restore(); + } + + double[] solveSystem(ControlPoint[] points) { + int n = points.length; + double[][] system = new double[n][3]; + double[] result = new double[n]; // d + double[] solution = new double[n]; // returned coefficients + system[0][1] = 1; + system[n - 1][1] = 1; + double d6 = 1.0 / 6.0; + double d3 = 1.0 / 3.0; + + // let's create a tridiagonal matrix representing the + // system, and apply the TDMA algorithm to solve it + // (see http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm) + for (int i = 1; i < n - 1; i++) { + double deltaPrevX = points[i].x - points[i - 1].x; + double deltaX = points[i + 1].x - points[i - 1].x; + double deltaNextX = points[i + 1].x - points[i].x; + double deltaNextY = points[i + 1].y - points[i].y; + double deltaPrevY = points[i].y - points[i - 1].y; + system[i][0] = d6 * deltaPrevX; // a_i + system[i][1] = d3 * deltaX; // b_i + system[i][2] = d6 * deltaNextX; // c_i + result[i] = (deltaNextY / deltaNextX) - (deltaPrevY / deltaPrevX); // d_i + } + + // Forward sweep + for (int i = 1; i < n; i++) { + // m = a_i/b_i-1 + double m = system[i][0] / system[i - 1][1]; + // b_i = b_i - m(c_i-1) + system[i][1] = system[i][1] - m * system[i - 1][2]; + // d_i = d_i - m(d_i-1) + result[i] = result[i] - m * result[i - 1]; + } + + // Back substitution + solution[n - 1] = result[n - 1] / system[n - 1][1]; + for (int i = n - 2; i >= 0; --i) { + solution[i] = (result[i] - system[i][2] * solution[i + 1]) / system[i][1]; + } + return solution; + } + + public int addPoint(float x, float y) { + return addPoint(new ControlPoint(x, y)); + } + + public int addPoint(ControlPoint v) { + mPoints.add(v); + Collections.sort(mPoints); + return mPoints.indexOf(v); + } + + public void deletePoint(int n) { + mPoints.remove(n); + if (mPoints.size() < 2) { + reset(); + } + Collections.sort(mPoints); + } + + public int getNbPoints() { + return mPoints.size(); + } + + public ControlPoint getPoint(int n) { + return mPoints.elementAt(n); + } + + public boolean isPointContained(float x, int n) { + for (int i = 0; i < n; i++) { + ControlPoint point = mPoints.elementAt(i); + if (point.x > x) { + return false; + } + } + for (int i = n + 1; i < mPoints.size(); i++) { + ControlPoint point = mPoints.elementAt(i); + if (point.x < x) { + return false; + } + } + return true; + } + + public Spline copy() { + Spline spline = new Spline(); + for (int i = 0; i < mPoints.size(); i++) { + ControlPoint point = mPoints.elementAt(i); + spline.addPoint(point.copy()); + } + return spline; + } + + public void show() { + Log.v(LOGTAG, "show curve " + this); + for (int i = 0; i < mPoints.size(); i++) { + ControlPoint point = mPoints.elementAt(i); + Log.v(LOGTAG, "point " + i + " is (" + point.x + ", " + point.y + ")"); + } + } + +} |