1 /* 2 * Copyright 2018 Google LLC All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.skar.examples.helloskar.app; 18 19 import android.graphics.Color; 20 import android.graphics.Path; 21 import android.graphics.PointF; 22 23 import com.google.skar.examples.helloskar.helpers.GestureHelper; 24 25 import java.util.ArrayList; 26 import java.util.HashMap; 27 import java.util.List; 28 import java.util.Map; 29 30 public class FingerPainting { 31 public static class BuiltPath { 32 public Path path; 33 public int color; 34 BuiltPath(Path p, int c)35 BuiltPath(Path p, int c) { 36 this.path = p; 37 this.color = c; 38 } 39 } 40 41 // Points obtained by touching the screen. The first point is always brought to (0,0). 42 // All subsequent points are translated by the same amount. 43 private ArrayList<PointF> points = new ArrayList<>(); 44 45 // Indices in points array that indicate the start of a new path. E.g: if 5 is added to 46 // jumpPoints, then index 5 of the points array is the start of a new path (use moveTo()) 47 private ArrayList<Integer> jumpPoints = new ArrayList<>(); 48 49 // Map from index (start of path) to color of path 50 private Map<Integer, Integer> indexColors = new HashMap<>(); 51 52 // List of built paths (reset each frame) 53 private ArrayList<BuiltPath> paths = new ArrayList<>(); 54 55 // Previous point added to the path. This points belongs to the path in local space. 56 private float[] previousLocalPoint = new float[2]; 57 58 // Previous point added to the path. This points belongs to the path in global space (i.e Pose) 59 private float[] previousGlobalPoint = new float[2]; 60 61 // Holds the model matrix of the first point added to such that the path can be drawn at the 62 // model location (i.e on the Plane) 63 private float[] modelMatrix; 64 65 // Currently selected color in the UI 66 private int color = Color.RED; 67 68 // True if path should be drawn using buildSmoothFromTo() 69 private boolean isSmooth; 70 FingerPainting(boolean smooth)71 public FingerPainting(boolean smooth) { 72 this.isSmooth = smooth; 73 } 74 setSmoothness(boolean smooth)75 public void setSmoothness(boolean smooth) { 76 isSmooth = smooth; 77 } 78 79 /** 80 * Given a hit location in Global space (e.g on a Plane), and the associated ScrollEvent, 81 * construct the next point in the path in Local space. The first point of the Finger Painting 82 * must be at (0,0) 83 * @param hitLocation (x, y) coordinates of the hit position in Global space 84 * @param holdTap ScrollEvent associated with the hit test that calls this function 85 * @return true if point was computed and added. False otherwise. 86 */ computeNextPoint(float[] hitLocation, float[] modelMat, GestureHelper.ScrollEvent holdTap)87 public boolean computeNextPoint(float[] hitLocation, float[] modelMat, GestureHelper.ScrollEvent holdTap) { 88 if (isEmpty()) { 89 // If finger painting is empty, then first point is origin. Model matrix 90 // of the finger painting is the model matrix of the first point 91 addPoint(new PointF(0, 0), true); 92 93 // Get model matrix of first point 94 setModelMatrix(modelMat); 95 } else { 96 // Else, construct next point given its distance from previous point 97 float localDistanceScale = 1000; 98 PointF distance = new PointF(hitLocation[0] - previousGlobalPoint[0], 99 hitLocation[2] - previousGlobalPoint[1]); 100 101 if (distance.length() < 0.01f) { 102 // If distance between previous stored point and current point is too 103 // small, skip it 104 return false; 105 } 106 107 // New point is distance + old point 108 PointF p = new PointF(distance.x * localDistanceScale + previousLocalPoint[0], 109 distance.y * localDistanceScale + previousLocalPoint[1]); 110 111 addPoint(p, holdTap.isStartOfScroll); 112 } 113 previousGlobalPoint[0] = hitLocation[0]; 114 previousGlobalPoint[1] = hitLocation[2]; 115 return true; 116 } 117 118 /** 119 * Constructs the Paths to be drawn (populates the paths List). Call this before drawing this 120 * Finger Painting (every frame). 121 */ buildPath()122 public void buildPath() { 123 if (points.size() <= 1) { 124 // Don't build anything if the path only contains one point 125 return; 126 } 127 128 paths = new ArrayList<>(); 129 130 if (isSmooth) { 131 buildSmooth(); 132 } else { 133 buildRough(); 134 } 135 } 136 137 /** 138 * @return 16-float matrix that takes a point from Local space to Global space (onto the Plane) 139 */ getModelMatrix()140 public float[] getModelMatrix() { 141 return modelMatrix; 142 } 143 144 /** 145 * Change currently selected color. Preferably called through a UI element (a menu) 146 * @param color color to be selected this frame 147 */ setColor(int color)148 public void setColor(int color) { 149 this.color = color; 150 } 151 152 /** 153 * @return List of built paths contained within this Finger Painting 154 */ getPaths()155 public List<BuiltPath> getPaths() { 156 return paths; 157 } 158 159 /** 160 * Clears data contained within this Finger Painting 161 */ reset()162 public void reset() { 163 points.clear(); 164 jumpPoints.clear(); 165 paths.clear(); 166 indexColors.clear(); 167 } 168 169 /********************** PRIVATE HELPERS **************************************/ 170 171 // Adds another point to the path in Local space addPoint(PointF p, boolean jumpPoint)172 private void addPoint(PointF p, boolean jumpPoint) { 173 points.add(p); 174 if (jumpPoint) { 175 jumpPoints.add(points.size() - 1); 176 indexColors.put(points.size() - 1, color); 177 } 178 previousLocalPoint[0] = p.x; 179 previousLocalPoint[1] = p.y; 180 } 181 182 // Builds paths of this Finger Painting using the rough algorithm buildRough()183 private void buildRough() { 184 int start = 0; // starting index of each path. 1st path starts at index 0 points list 185 for (int j = 1; j < jumpPoints.size(); j++) { 186 int finish = jumpPoints.get(j); // finishing index of current path 187 buildRoughFromTo(start, finish); 188 start = finish; 189 } 190 191 buildRoughFromTo(start, points.size()); 192 } 193 194 // Builds paths of this Finger Painting using the smooth algorithm buildSmooth()195 private void buildSmooth() { 196 int start = 0; 197 for (int j = 1; j < jumpPoints.size(); j++) { 198 int finish = jumpPoints.get(j); 199 buildSmoothFromTo(start, finish); 200 start = finish; 201 } 202 203 buildSmoothFromTo(start, points.size()); 204 } 205 206 // Builds a rough path that starts at index (start) of the points List, and ends at (finish - 1) 207 // of the points List buildRoughFromTo(int start, int finish)208 private void buildRoughFromTo(int start, int finish) { 209 Path p = new Path(); 210 int c = indexColors.get(start); 211 p.moveTo(points.get(start).x, points.get(start).y); 212 213 for (int i = start + 1; i < finish; i++) { 214 p.lineTo(points.get(i).x, points.get(i).y); 215 } 216 217 BuiltPath bp = new BuiltPath(p, c); 218 paths.add(bp); 219 } 220 221 // Builds a smooth path that starts at index (start) of the points List, and ends at (finish - 1) 222 // of the points List buildSmoothFromTo(int start, int finish)223 private void buildSmoothFromTo(int start, int finish) { 224 Path p = new Path(); 225 int c = indexColors.get(start); 226 227 int nbPts = finish - start; // # of points within this path (not including the finish index) 228 229 // If only 2 points in path, draw a line between them 230 if (nbPts == 2) { 231 p.moveTo(points.get(start).x, points.get(start).y); 232 p.lineTo(points.get(start + 1).x, points.get(start + 1).y); 233 BuiltPath bp = new BuiltPath(p, c); 234 paths.add(bp); 235 } else if (nbPts >= 3) { 236 // Else (3 pts +), essentially run deCasteljau 237 p.moveTo(points.get(start).x, points.get(start).y); 238 p.lineTo((points.get(start).x + points.get(start + 1).x) / 2, 239 (points.get(start).y + points.get(start + 1).y) / 2); 240 241 for (int i = start + 1; i < finish - 1; i++) { 242 PointF p1 = points.get(i); 243 PointF p2 = points.get(i + 1); 244 p.quadTo(p1.x, p1.y, (p1.x + p2.x) / 2, (p1.y + p2.y) / 2); 245 } 246 247 p.lineTo(points.get(finish - 1).x, points.get(finish - 1).y); 248 BuiltPath bp = new BuiltPath(p, c); 249 paths.add(bp); 250 } 251 } 252 isEmpty()253 private boolean isEmpty() { return points.isEmpty(); } 254 setModelMatrix(float[] m)255 private void setModelMatrix(float[] m) { 256 modelMatrix = m; 257 } 258 } 259