• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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