blob: 48b91615d44bf317d0c120b7431f7552c81d4427 [file] [log] [blame]
/*
* 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;
}
}