/* * Copyright 2018 Google LLC All Rights Reserved. * * 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.google.skar.examples.helloskar.app; import android.graphics.Color; import android.graphics.Path; import android.graphics.PointF; import com.google.skar.examples.helloskar.helpers.GestureHelper; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class FingerPainting { public static class BuiltPath { public Path path; public int color; BuiltPath(Path p, int c) { this.path = p; this.color = c; } } // Points obtained by touching the screen. The first point is always brought to (0,0). // All subsequent points are translated by the same amount. private ArrayList points = new ArrayList<>(); // Indices in points array that indicate the start of a new path. E.g: if 5 is added to // jumpPoints, then index 5 of the points array is the start of a new path (use moveTo()) private ArrayList jumpPoints = new ArrayList<>(); // Map from index (start of path) to color of path private Map indexColors = new HashMap<>(); // List of built paths (reset each frame) private ArrayList paths = new ArrayList<>(); // Previous point added to the path. This points belongs to the path in local space. private float[] previousLocalPoint = new float[2]; // Previous point added to the path. This points belongs to the path in global space (i.e Pose) private float[] previousGlobalPoint = new float[2]; // Holds the model matrix of the first point added to such that the path can be drawn at the // model location (i.e on the Plane) private float[] modelMatrix; // Currently selected color in the UI private int color = Color.RED; // True if path should be drawn using buildSmoothFromTo() private boolean isSmooth; public FingerPainting(boolean smooth) { this.isSmooth = smooth; } public void setSmoothness(boolean smooth) { isSmooth = smooth; } /** * Given a hit location in Global space (e.g on a Plane), and the associated ScrollEvent, * construct the next point in the path in Local space. The first point of the Finger Painting * must be at (0,0) * @param hitLocation (x, y) coordinates of the hit position in Global space * @param holdTap ScrollEvent associated with the hit test that calls this function * @return true if point was computed and added. False otherwise. */ public boolean computeNextPoint(float[] hitLocation, float[] modelMat, GestureHelper.ScrollEvent holdTap) { if (isEmpty()) { // If finger painting is empty, then first point is origin. Model matrix // of the finger painting is the model matrix of the first point addPoint(new PointF(0, 0), true); // Get model matrix of first point setModelMatrix(modelMat); } else { // Else, construct next point given its distance from previous point float localDistanceScale = 1000; PointF distance = new PointF(hitLocation[0] - previousGlobalPoint[0], hitLocation[2] - previousGlobalPoint[1]); if (distance.length() < 0.01f) { // If distance between previous stored point and current point is too // small, skip it return false; } // New point is distance + old point PointF p = new PointF(distance.x * localDistanceScale + previousLocalPoint[0], distance.y * localDistanceScale + previousLocalPoint[1]); addPoint(p, holdTap.isStartOfScroll); } previousGlobalPoint[0] = hitLocation[0]; previousGlobalPoint[1] = hitLocation[2]; return true; } /** * Constructs the Paths to be drawn (populates the paths List). Call this before drawing this * Finger Painting (every frame). */ public void buildPath() { if (points.size() <= 1) { // Don't build anything if the path only contains one point return; } paths = new ArrayList<>(); if (isSmooth) { buildSmooth(); } else { buildRough(); } } /** * @return 16-float matrix that takes a point from Local space to Global space (onto the Plane) */ public float[] getModelMatrix() { return modelMatrix; } /** * Change currently selected color. Preferably called through a UI element (a menu) * @param color color to be selected this frame */ public void setColor(int color) { this.color = color; } /** * @return List of built paths contained within this Finger Painting */ public List getPaths() { return paths; } /** * Clears data contained within this Finger Painting */ public void reset() { points.clear(); jumpPoints.clear(); paths.clear(); indexColors.clear(); } /********************** PRIVATE HELPERS **************************************/ // Adds another point to the path in Local space private void addPoint(PointF p, boolean jumpPoint) { points.add(p); if (jumpPoint) { jumpPoints.add(points.size() - 1); indexColors.put(points.size() - 1, color); } previousLocalPoint[0] = p.x; previousLocalPoint[1] = p.y; } // Builds paths of this Finger Painting using the rough algorithm private void buildRough() { int start = 0; // starting index of each path. 1st path starts at index 0 points list for (int j = 1; j < jumpPoints.size(); j++) { int finish = jumpPoints.get(j); // finishing index of current path buildRoughFromTo(start, finish); start = finish; } buildRoughFromTo(start, points.size()); } // Builds paths of this Finger Painting using the smooth algorithm private void buildSmooth() { int start = 0; for (int j = 1; j < jumpPoints.size(); j++) { int finish = jumpPoints.get(j); buildSmoothFromTo(start, finish); start = finish; } buildSmoothFromTo(start, points.size()); } // Builds a rough path that starts at index (start) of the points List, and ends at (finish - 1) // of the points List private void buildRoughFromTo(int start, int finish) { Path p = new Path(); int c = indexColors.get(start); p.moveTo(points.get(start).x, points.get(start).y); for (int i = start + 1; i < finish; i++) { p.lineTo(points.get(i).x, points.get(i).y); } BuiltPath bp = new BuiltPath(p, c); paths.add(bp); } // Builds a smooth path that starts at index (start) of the points List, and ends at (finish - 1) // of the points List private void buildSmoothFromTo(int start, int finish) { Path p = new Path(); int c = indexColors.get(start); int nbPts = finish - start; // # of points within this path (not including the finish index) // If only 2 points in path, draw a line between them if (nbPts == 2) { p.moveTo(points.get(start).x, points.get(start).y); p.lineTo(points.get(start + 1).x, points.get(start + 1).y); BuiltPath bp = new BuiltPath(p, c); paths.add(bp); } else if (nbPts >= 3) { // Else (3 pts +), essentially run deCasteljau p.moveTo(points.get(start).x, points.get(start).y); p.lineTo((points.get(start).x + points.get(start + 1).x) / 2, (points.get(start).y + points.get(start + 1).y) / 2); for (int i = start + 1; i < finish - 1; i++) { PointF p1 = points.get(i); PointF p2 = points.get(i + 1); p.quadTo(p1.x, p1.y, (p1.x + p2.x) / 2, (p1.y + p2.y) / 2); } p.lineTo(points.get(finish - 1).x, points.get(finish - 1).y); BuiltPath bp = new BuiltPath(p, c); paths.add(bp); } } private boolean isEmpty() { return points.isEmpty(); } private void setModelMatrix(float[] m) { modelMatrix = m; } }