aboutsummaryrefslogtreecommitdiffhomepage
path: root/platform_tools/android/apps/skar_java/src/main/java/com/google/skar/examples/helloskar/app/FingerPainting.java
blob: 48b91615d44bf317d0c120b7431f7552c81d4427 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/*
 * 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<PointF> 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<Integer> jumpPoints = new ArrayList<>();

    // Map from index (start of path) to color of path
    private Map<Integer, Integer> indexColors = new HashMap<>();

    // List of built paths (reset each frame)
    private ArrayList<BuiltPath> 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<BuiltPath> 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;
    }
}