package info.u250.c2d.tests.mesh; import info.u250.c2d.engine.Engine; import info.u250.c2d.engine.EngineDrive; import info.u250.c2d.engine.Scene; import info.u250.c2d.engine.resources.AliasResourceManager; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.InputAdapter; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.graphics.Camera; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture.TextureFilter; import com.badlogic.gdx.graphics.glutils.ImmediateModeRenderer20; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; public class FingerSwipeTest extends Engine { @Override protected EngineDrive onSetupEngineDrive() { return new EngineX(); } @Override public void dispose () { super.dispose(); } private class EngineX implements EngineDrive{ @Override public void onResourcesRegister(AliasResourceManager<String> reg) { reg.texture("RES", "data/gradient.png"); } @Override public void dispose() {} @Override public EngineOptions onSetupEngine() { final EngineOptions opt = new EngineOptions(new String[]{"data/gradient.png"},800,480); opt.useGL20 = true; return opt; } @Override public void onLoadedResourcesCompleted() { final SwipeHandler swipe = new SwipeHandler(10);; final Texture tex = Engine.resource("RES"); final ShapeRenderer shapes = Engine.getShapeRenderer(); final SwipeTriStrip tris = new SwipeTriStrip(); //minimum distance between two points swipe.minDistance = 10; //minimum distance between first and second point swipe.initialDistance = 10; //we will use a texture for the smooth edge, and also for stroke effects tex.setFilter(TextureFilter.Linear, TextureFilter.Linear); Engine.setMainScene(new Scene() { @Override public void render(float delta) { Gdx.gl.glEnable(GL20.GL_BLEND); Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); tex.bind(); tris.endcap = 5f; //the thickness of the line tris.thickness = 30f; //generate the triangle strip from our path tris.update(swipe.path()); //the vertex color for tinting, i.e. for opacity tris.color = Color.WHITE; //render the triangles to the screen tris.draw(Engine.getDefaultCamera()); drawDebug(); } //optional debug drawing.. public void drawDebug() { Array<Vector2> input = swipe.input(); //draw the raw input shapes.begin(ShapeType.Line); shapes.setColor(Color.GRAY); for (int i=0; i<input.size-1; i++) { Vector2 p = input.get(i); Vector2 p2 = input.get(i+1); shapes.line(p.x, p.y, p2.x, p2.y); } shapes.end(); //draw the smoothed and simplified path shapes.begin(ShapeType.Line); shapes.setColor(Color.RED); Array<Vector2> out = swipe.path(); for (int i=0; i<out.size-1; i++) { Vector2 p = out.get(i); Vector2 p2 = out.get(i+1); shapes.line(p.x, p.y, p2.x, p2.y); } shapes.end(); //render our perpendiculars shapes.begin(ShapeType.Line); Vector2 perp = new Vector2(); for (int i=1; i<input.size-1; i++) { Vector2 p = input.get(i); Vector2 p2 = input.get(i+1); shapes.setColor(Color.LIGHT_GRAY); perp.set(p).sub(p2).nor(); perp.set(perp.y, -perp.x); perp.scl(10f); shapes.line(p.x, p.y, p.x+perp.x, p.y+perp.y); perp.scl(-1f); shapes.setColor(Color.BLUE); shapes.line(p.x, p.y, p.x+perp.x, p.y+perp.y); } shapes.end(); } @Override public InputProcessor getInputProcessor() { return swipe; } @Override public void update(float delta) { } @Override public void hide() { } @Override public void show() { } }); } } public static class FixedList<T> extends Array<T> { /** * Safely creates a list that is backed by an array which * can be directly accessed. * * @param capacity the fixed-size capacity of this list * @param type the class type of the elements in this list */ public FixedList(int capacity, Class<T> type) { super(false, capacity, type); } /** * Inserts the item into index zero, shifting all items to the right, * but without increasing the list's size past its array capacity. * @param t the element to insert */ public void insert(T t) { T[] items = this.items; // increase size if we have a new point size = Math.min(size + 1, items.length); // shift elements right for (int i = size - 1; i > 0; i--) { items[i] = items[i - 1]; } // insert new item at first index items[0] = t; } } public static class ResolverRadialChaikin{ private Array<Vector2> tmp = new Array<Vector2>(Vector2.class); public static int iterations = 2; public static float simplifyTolerance = 35f; public void resolve(Array<Vector2> input, Array<Vector2> output) { output.clear(); if (input.size<=2) { //simple copy output.addAll(input); return; } //simplify with squared tolerance if (simplifyTolerance>0 && input.size>3) { simplify(input, simplifyTolerance * simplifyTolerance, tmp); input = tmp; } //perform smooth operations if (iterations<=0) { //no smooth, just copy input to output output.addAll(input); } else if (iterations==1) { //1 iteration, smooth to output smooth(input, output); } else { //multiple iterations.. ping-pong between arrays int iters = iterations; //subsequent iterations do { smooth(input, output); tmp.clear(); tmp.addAll(output); Array<Vector2> old = output; input = tmp; output = old; } while (--iters > 0); } } public static void smooth(Array<Vector2> input, Array<Vector2> output) { //expected size output.clear(); output.ensureCapacity(input.size*2); //first element output.add(input.get(0)); //average elements for (int i=0; i<input.size-1; i++) { Vector2 p0 = input.get(i); Vector2 p1 = input.get(i+1); Vector2 Q = new Vector2(0.75f * p0.x + 0.25f * p1.x, 0.75f * p0.y + 0.25f * p1.y); Vector2 R = new Vector2(0.25f * p0.x + 0.75f * p1.x, 0.25f * p0.y + 0.75f * p1.y); output.add(Q); output.add(R); } //last element output.add(input.get(input.size-1)); } //simple distance-based simplification //adapted from simplify.js public static void simplify(Array<Vector2> points, float sqTolerance, Array<Vector2> out) { int len = points.size; Vector2 point = new Vector2(); Vector2 prevPoint = points.get(0); out.clear(); out.add(prevPoint); for (int i = 1; i < len; i++) { point = points.get(i); if (distSq(point, prevPoint) > sqTolerance) { out.add(point); prevPoint = point; } } if (!prevPoint.equals(point)) { out.add(point); } } public static float distSq(Vector2 p1, Vector2 p2) { float dx = p1.x - p2.x, dy = p1.y - p2.y; return dx * dx + dy * dy; } } public static class SwipeHandler extends InputAdapter { private FixedList<Vector2> inputPoints; /** The pointer associated with this swipe event. */ private int inputPointer = 0; /** The minimum distance between the first and second point in a drawn line. */ public int initialDistance = 10; /** The minimum distance between two points in a drawn line (starting at the second point). */ public int minDistance = 20; private Vector2 lastPoint = new Vector2(); private boolean isDrawing = false; private ResolverRadialChaikin simplifier = new ResolverRadialChaikin(); private Array<Vector2> simplified; public SwipeHandler(int maxInputPoints) { this.inputPoints = new FixedList<Vector2>(maxInputPoints, Vector2.class); simplified = new Array<Vector2>(true, maxInputPoints, Vector2.class); resolve(); //copy initial empty list } /** * Returns the fixed list of input points (not simplified). * @return the list of input points */ public Array<Vector2> input() { return this.inputPoints; } /** * Returns the simplified list of points representing this swipe. * @return */ public Array<Vector2> path() { return simplified; } /** * If the points are dirty, the line is simplified. */ public void resolve() { simplifier.resolve(inputPoints, simplified); } public boolean touchDown(int screenX, int screenY, int pointer, int button) { if (pointer!=inputPointer) return false; isDrawing = true; //clear points inputPoints.clear(); //starting point lastPoint = new Vector2(screenX, Gdx.graphics.getHeight()-screenY); inputPoints.insert(lastPoint); resolve(); return true; } public boolean touchUp(int screenX, int screenY, int pointer, int button) { //on release, the line is simplified resolve(); isDrawing = false; return false; } public boolean touchDragged(int screenX, int screenY, int pointer) { if (pointer!=inputPointer) return false; isDrawing = true; Vector2 v = new Vector2(screenX, Gdx.graphics.getHeight()-screenY); //calc length float dx = v.x - lastPoint.x; float dy = v.y - lastPoint.y; float len = (float)Math.sqrt(dx*dx + dy*dy); //TODO: use minDistanceSq //if we are under required distance if (len < minDistance && (inputPoints.size>1 || len<initialDistance)) return false; //add new point inputPoints.insert(v); lastPoint = v; //simplify our new line resolve(); return true; } public boolean isDrawing() { return isDrawing; } public void setDrawing(boolean isDrawing) { this.isDrawing = isDrawing; } } public static class SwipeTriStrip { Array<Vector2> texcoord = new Array<Vector2>(); Array<Vector2> tristrip = new Array<Vector2>(); int batchSize; Vector2 perp = new Vector2(); public float thickness = 30f; public float endcap = 8.5f; public Color color = new Color(Color.WHITE); ImmediateModeRenderer20 gl20; public SwipeTriStrip() { gl20 = new ImmediateModeRenderer20(false, true, 1); } public void draw(Camera cam) { if (tristrip.size<=0) return; gl20.begin(cam.combined, GL20.GL_TRIANGLE_STRIP); for (int i=0; i<tristrip.size; i++) { if (i==batchSize) { gl20.end(); gl20.begin(cam.combined, GL20.GL_TRIANGLE_STRIP); } Vector2 point = tristrip.get(i); Vector2 tc = texcoord.get(i); gl20.color(color.r, color.g, color.b, color.a); gl20.texCoord(tc.x, 0f); gl20.vertex(point.x, point.y, 0f); } gl20.end(); } private int generate(Array<Vector2> input, int mult) { int c = tristrip.size; if (endcap<=0) { tristrip.add(input.get(0)); } else { Vector2 p = input.get(0); Vector2 p2 = input.get(1); perp.set(p).sub(p2).scl(endcap); tristrip.add(new Vector2(p.x+perp.x, p.y+perp.y)); } texcoord.add(new Vector2(0f, 0f)); for (int i=1; i<input.size-1; i++) { Vector2 p = input.get(i); Vector2 p2 = input.get(i+1); //get direction and normalize it perp.set(p).sub(p2).nor(); //get perpendicular perp.set(-perp.y, perp.x); float thick = thickness * (1f-((i)/(float)(input.size))); //move outward by thickness perp.scl(thick/2f); //decide on which side we are using perp.scl(mult); //add the tip of perpendicular tristrip.add(new Vector2(p.x+perp.x, p.y+perp.y)); //0.0 -> end, transparent texcoord.add(new Vector2(0f, 0f)); //add the center point tristrip.add(new Vector2(p.x, p.y)); //1.0 -> center, opaque texcoord.add(new Vector2(1f, 0f)); } //final point if (endcap<=0) { tristrip.add(input.get(input.size-1)); } else { Vector2 p = input.get(input.size-2); Vector2 p2 = input.get(input.size-1); perp.set(p2).sub(p).scl(endcap); tristrip.add(new Vector2(p2.x+perp.x, p2.y+perp.y)); } //end cap is transparent texcoord.add(new Vector2(0f, 0f)); return tristrip.size-c; } public void update(Array<Vector2> input) { tristrip.clear(); texcoord.clear(); if (input.size<2) return; batchSize = generate(input, 1); generate(input, -1); } } }