Get font outlines programmatically - java

Is it somehow possible to retrieve font (ttf/otf) outline as a curve/series of points on Android? For example, if I wanted to convert a word with specific font into a vector format?

Since I never developed for an Android device, I will give you the way to do that, but in Java.
This is a couple of good libraries, but I don't know if it would be possible for you to use it (C/C++) So I will explain you how to do it yourself.
You should in first convert your word in a shape using a TextLayout (an immutable graphical representation of styled character data that you can draw) in a FontRenderContext.
According to the John J Smith answer here: https://stackoverflow.com/a/6864113/837765, It should be possible to use something similar to a TextLayout on Android. But, there's no quivalent of FontRenderContext. As I said, I never developed for an Android device, but there is probably (I hope so) a workaround to convert characters in a shape.
In Java Something like this should work (to convert text in a Shape):
public Shape getShape(String text, Font font, Point from) {
FontRenderContext context = new FontRenderContext(null, false, false);
GeneralPath shape = new GeneralPath();
TextLayout layout = new TextLayout(text, font, context);
Shape outline = layout.getOutline(null);
shape.append(outline, true);
return shape;
}
Then, you should find the shape boundary. It's not pretty difficult here, because your shape can give you directly the path iterator with shape.getPathIterator(null)
On each iteration, you can get the current segment, its type and the coordinates.:
SEG_QUADTO: a quadratic parametric curve;
SEG_CUBICTO : a cubic parametric curve;
SEG_LINETO : specifies the end point of a line;
SEG_MOVETO : a point that specifies the starting location for a new subpath.
At this point, you should read about Bézier curve here and here.
You will learn that:
Any quadratic spline can be expressed as a cubic (where the cubic term
is zero). The end points of the cubic will be the same as the
quadratic's.
CP0 = QP0 CP3 = QP2
The two control points for the cubic are:
CP1 = QP0 + 2/3 *(QP1-QP0) CP2 = CP1 + 1/3 *(QP2-QP0)
So converting from TrueType to PostScript is trivial.
In Java Something like this should work:
public List<Point> getPoints(Shape shape) {
List<Point> out = new ArrayList<Point>();
PathIterator iterator = shape.getPathIterator(null);
double[] coordinates = new double[6];
double x = 0, y = 0;
while (!iterator.isDone()) {
double x1 = coordinates[0];
double y1 = coordinates[1];
double x2 = coordinates[2];
double y2 = coordinates[3];
double x3 = coordinates[4];
double y3 = coordinates[5];
switch (iterator.currentSegment(coordinates)) {
case PathIterator.SEG_QUADTO:
x3 = x2;
y3 = y2;
x2 = x1 + 1 / 3f * (x2 - x1);
y2 = y1 + 1 / 3f * (y2 - y1);
x1 = x + 2 / 3f * (x1 - x);
y1 = y + 2 / 3f * (y1 - y);
out.add(new Point(x3, y3));
x = x3;
y = y3;
break;
case PathIterator.SEG_CUBICTO:
out.add(new Point(x3, y3));
x = x3;
y = y3;
break;
case PathIterator.SEG_LINETO:
out.add(new Point(x1, y1));
x = x1;
y = y1;
break;
case PathIterator.SEG_MOVETO:
out.add(new Point(x1, y1));
x = x1;
y = y1;
break;
}
iterator.next();
}
return out;
}
I created a demo project on Bitbucket, maybe it could help you.
https://bitbucket.org/pieralexandre/fontshape
Initial shape text (after outline transformation):
The points on the shape:
Only the points:
And the output of all points:
(0.0,0.0)
(9.326171875,200.0)
(9.326171875,127.734375)
(0.0,0.0)
(50.7080078125,130.126953125)
(62.158203125,138.232421875)
(69.82421875,162.158203125)
(60.302734375,190.087890625)
(50.78125,200.0)
//...
I know you cannot use Graphics2D (I used it for the UI) in Android, but you should be able to use my solution for an Android Project (I hope).
After that, you could be able (with the help of Bézier curve) to recreate your curves.
Also, there's a couple of other good tools here. take a look at this one:
http://nodebox.github.io/opentype.js/
It's in Javascript, but maybe it could help you even more.

Related

Trouble converting jbox2d angle to slick2d angle

UPDATE
Slick and JBox use radians that go in opposite directions, that's why I was having trouble.
I am making a game using JBox2D and Slick2D (per the title). So, because I couldn't find anything online about it, I wrote a bunch of code from scratch to convert between them. However, it seems as though the angles are different, even though both documentations say they use radians.
Here is my code:
//In the update function
angle = (float) (angle % 2*Math.PI);
mass = player.getMass();
position = player.getPosition();
if(input.isKeyDown(inputLeft)){
angle-=0.015f*turnBlocks.size()/mass; //turning, pt1
} else if(input.isKeyDown(inputRight)){
angle+=0.015f*turnBlocks.size()/mass;
}
player.setTransform(position, angle); //turning, pt2
if(input.isKeyDown(inputForward)){
float xv = (float)(0.25f * Math.sin(angle) *
thrustBlocks.size() / mass); //Converting angle to vector
float yv = (float)(0.25f * Math.cos(angle) *
thrustBlocks.size() / mass);
Vec2 curVel = player.getLinearVelocity();
xv = xv + curVel.x;
yv = yv + curVel.y;
player.setLinearVelocity(new Vec2(xv, yv));
}
and
//In the render function
g.setColor(Color.gray);
for(int mass = 0; mass < massBlocks.size(); mass++){
float boxx = (float)massBlocks.get(mass)[0];
float boxy = (float)massBlocks.get(mass)[1];
int[] slicklist = tr.toSlick(position.x+boxx, position.y+boxy);
boxx = (float)slicklist[0];
boxy = (float)slicklist[1];
float[] ps = {boxx-tr.xscale/2, boxy-tr.yscale/2,
boxx+tr.xscale/2, boxy-tr.yscale/2,
boxx+tr.xscale/2, boxy+tr.yscale/2,
boxx-tr.xscale/2, boxy+tr.yscale/2};
Polygon p = new Polygon(ps);
//turning, pt3
g.fill(p.transform(Transform.createRotateTransform(radAngle, slickx, slicky)));
}
When I run the above code (with the rest of it), I get the player block(s) moving in the direction it shows it is facing. However, the collision in Jbox2D is out of sync. Here is the pattern I have found:
1 unit = pi/4 in slick
Slick direction:
7___0___1
6___.___2
5___4___3
Jbox Direction:
5___0___3
2___.___6
7___4___1
Really, I have no idea what is going on. Can somebody help?
Okay. It turns out that even thought Slick's transform and JBox's angle are both radians, They go in opposite directions. So, I made the below code with the .getWorldPosition instead of transform.
float localJBoxX = thrustBlocks.get(count)[0];
float localJBoxY = thrustBlocks.get(count)[1];
float[] localEndCoords = {localJBoxX+0.5f, localJBoxY+0.5f,
localJBoxX-0.5f, localJBoxY+0.5f,
localJBoxX-0.5f, localJBoxY-0.5f,
localJBoxX+0.5f, localJBoxY-0.5f};
float[] slickCoords = new float[localEndCoords.length];
for(byte point = 0; point<localEndCoords.length/2; point++){
Vec2 localPoint = new Vec2(localEndCoords[point*2], localEndCoords[point*2+1]);
slickCoords[point*2] = (float)tr.toSlick(player.getWorldPoint(localPoint).x, player.getWorldPoint(localPoint).y)[0];
slickCoords[point*2+1] = (float)tr.toSlick(player.getWorldPoint(localPoint).x, player.getWorldPoint(localPoint).y)[1];
}
Polygon box = new Polygon(slickCoords);
g.fill(box.transform(new Transform())); //as to return a shape

smooth color interpolation along a "bresenham" line

I am trying to interpolate color along a line so that, given two points and their respective RGB values, I can draw a line with a smooth color gradient. Using Bresenham's Line Algorithm, I can now draw lines, but am not sure how to begin interpolating colors between the two end points. The following is part of the drawLine() function that works for all line whose slope are less than 1.
int x_start = p1.x, x_end = p2.x, y_start =p1.y, y_end = p2.y;
int dx = Math.abs(x_end-x_start), dy = Math.abs(y_end-y_start);
int x = x_start, y = y_start;
int step_x = x_start < x_end ? 1:-1;
int step_y = y_start < y_end ? 1:-1;
int rStart = (int)(255.0f * p1.c.r), rEnd = (int)(255.0f * p2.c.r);
int gStart = (int)(255.0f * p1.c.g), gEnd = (int)(255.0f * p2.c.g);
int bStart = (int)(255.0f * p1.c.b), bEnd = (int)(255.0f * p2.c.b);
int xCount = 0;
//for slope < 1
int p = 2*dy-dx;
int twoDy = 2*dy, twoDyMinusDx = 2*(dy-dx);
int xCount = 0;
// draw the first point
Point2D start = new Point2D(x, y, new ColorType(p1.c.r, p1.c.g, p1.c.b));
drawPoint(buff, start);
float pColor = xCount / Math.abs((x_end - x_start));
System.out.println(x_end + " " + x_start);
while(x != x_end){
x+= step_x;
xCount++;
if(p<0){
p+= twoDy;
}
else{
y += step_y;
p += twoDyMinusDx;
}
Point2D draw_line = new Point2D(x, y, new ColorType(p1.c.r*(1-pColor)+p2.c.r*pColor,p1.c.g*(1-pColor)+p2.c.g*pColor,p1.c.b*(1-pColor)+p2.c.b*pColor));
System.out.println(pColor);
drawPoint(buff,draw_line );
}
So what I'm thinking is that, just like drawing lines, I also need some sort of decision parameter p to determine when to change the RGB values. I am thinking of something along lines of as x increments, look at each rgb value and decide if I want to manipualte them or not.
I initialized rStart and rEnd(and so on for g and b) but have no idea where to start. any kind of help or suggestions would be greatly appreciated!
Edit: thanks #Compass for the great suggestion ! Now I've ran into another while trying to implementing that strategy, and I am almost certain it's an easy bug. I just can't see it right now. For some reason my pColor always return 0, I am not sure why. I ran some print statements to make sure xCount is indeed increasing, so I am not sure what else might've made this variable always 0.
I remember figuring this out way back when I was learning GUI! I'll explain the basic concepts for you.
Let's say we have two colors,
RGB(A,B,C)
and
RGB(X,Y,Z)
for simplicity.
If we know the position percentage-wise (we'll call this P, a float 0 for beginning, 1.0 at end) along the line, we can calculate what color should be there using the following:
Resultant Color = RGB(A*(1-P)+X*P,B*(1-P)+Y*P,C*(1-P)+Z*P)
In other words, you average out the individual RGB values along the line.
Actually you will be drawing the line in RGB space as well !
Bresenham lets you compute point coordinates from (X0, Y0) to (X1, Y1).
This is done by a loop on X or Y, with a linear interpolation on the other coordinate.
Just extend the algorithm to draw a line from (X0, Y0, R0, G0, B0) to (X1, Y1, R1, G1, B1), in the same loop on X or Y, with a linear interpolation on the other coordinates.

Transform + scale a set of points

I wrote a gui where a user draws something in a (640x480) window. It makes that drawing into a set of points stored in a Vector array. Now, how do I translate those set of points to the origin (0,0 top left corner of the window) or put it at a specified pos? The width and height of the window I want it in is also 640x480.
After that is solved, how do you scale that new set of points to a size I want?
UPDATE 1
I solved the scale issue, but not the positioning issue. The drawing is not going where I tell it to be. Code below of what I have so far.
float scaleX = (float)width/boundingPoints.width;
float scaleY = (float)height/boundingPoints.height;
for(int i = 0; i < cg_points.size()-1; i++){
Point p1 = cg_points.get(i);
Point p2 = cg_points.get(i+1);
g.drawLine((int)(p1.x*scaleX) + pos.x, (int)(p1.y*scaleY) + pos.y, (int)(p2.x*scaleX) + pos.x, (int)(p2.y*scaleY) + pos.y);
}
I want the drawing to start at where pos [x, y] is. What is currently the problem is this. It does follow what pos.x and pos.y does, but it is way off and not starting at pos[x,y].
Here is a screen shot of the issue
As you can see from the picture, the box is where the star is supposed to be. The scaling is right as you can see, just not the pos. That is because the points in the drawing may NOT start at (0,0).
Any suggestions?
Thanks!
To translate a drawing, simply
foreach point in array
point.x += translate.x
point.y += translate.y
If you're going to center a drawing, pick a center (such as averaging all your points), negate that value, then translate all your points by that value.
To scale a drawing:
foreach point in array
point.x *= scale
point.y *= scale
So I solved it...YAY!!! Here is the code below in case you run into the same issue as I had.
float scaleX = (float)width/boundingPoints.width;
float scaleY = (float)height/boundingPoints.height;
int bx = boundingPoints.x;
int by = boundingPoints.y;
for(int i = 0; i < cg_points.size()-1; i++){
Point p1 = cg_points.get(i);
Point p2 = cg_points.get(i+1);
int x1 = (int) ((p1.x-bx)*scaleX);
x1 += pos.x;
int y1 = (int) ((p1.y-by)*scaleY);
y1 += pos.y;
int x2 = (int) ((p2.x-bx)*scaleX);
x2 += pos.x;
int y2 = (int) ((p2.y-by)*scaleY);
y2 += pos.y;
g.drawLine(x1, y1, x2, y2);
}

How to draw a smooth line through a set of points using Bezier curves?

I need to draw a smooth line through a set of vertices. The set of vertices is compiled by a user dragging their finger across a touch screen, the set tends to be fairly large and the distance between the vertices is fairly small. However, if I simply connect each vertex with a straight line, the result is very rough (not-smooth).
I found solutions to this which use spline interpolation (and/or other things I don't understand) to smooth the line by adding a bunch of additional vertices. These work nicely, but because the list of vertices is already fairly large, increasing it by 10x or so has significant performance implications.
It seems like the smoothing should be accomplishable by using Bezier curves without adding additional vertices.
Below is some code based on the solution here:
http://www.antigrain.com/research/bezier_interpolation/
It works well when the distance between the vertices is large, but doesn't work very well when the vertices are close together.
Any suggestions for a better way to draw a smooth curve through a large set of vertices, without adding additional vertices?
Vector<PointF> gesture;
protected void onDraw(Canvas canvas)
{
if(gesture.size() > 4 )
{
Path gesturePath = new Path();
gesturePath.moveTo(gesture.get(0).x, gesture.get(0).y);
gesturePath.lineTo(gesture.get(1).x, gesture.get(1).y);
for (int i = 2; i < gesture.size() - 1; i++)
{
float[] ctrl = getControlPoint(gesture.get(i), gesture.get(i - 1), gesture.get(i), gesture.get(i + 1));
gesturePath.cubicTo(ctrl[0], ctrl[1], ctrl[2], ctrl[3], gesture.get(i).x, gesture.get(i).y);
}
gesturePath.lineTo(gesture.get(gesture.size() - 1).x, gesture.get(gesture.size() - 1).y);
canvas.drawPath(gesturePath, mPaint);
}
}
}
private float[] getControlPoint(PointF p0, PointF p1, PointF p2, PointF p3)
{
float x0 = p0.x;
float x1 = p1.x;
float x2 = p2.x;
float x3 = p3.x;
float y0 = p0.y;
float y1 = p1.y;
float y2 = p2.y;
float y3 = p3.y;
double xc1 = (x0 + x1) / 2.0;
double yc1 = (y0 + y1) / 2.0;
double xc2 = (x1 + x2) / 2.0;
double yc2 = (y1 + y2) / 2.0;
double xc3 = (x2 + x3) / 2.0;
double yc3 = (y2 + y3) / 2.0;
double len1 = Math.sqrt((x1-x0) * (x1-x0) + (y1-y0) * (y1-y0));
double len2 = Math.sqrt((x2-x1) * (x2-x1) + (y2-y1) * (y2-y1));
double len3 = Math.sqrt((x3-x2) * (x3-x2) + (y3-y2) * (y3-y2));
double k1 = len1 / (len1 + len2);
double k2 = len2 / (len2 + len3);
double xm1 = xc1 + (xc2 - xc1) * k1;
double ym1 = yc1 + (yc2 - yc1) * k1;
double xm2 = xc2 + (xc3 - xc2) * k2;
double ym2 = yc2 + (yc3 - yc2) * k2;
// Resulting control points. Here smooth_value is mentioned
// above coefficient K whose value should be in range [0...1].
double k = .1;
float ctrl1_x = (float) (xm1 + (xc2 - xm1) * k + x1 - xm1);
float ctrl1_y = (float) (ym1 + (yc2 - ym1) * k + y1 - ym1);
float ctrl2_x = (float) (xm2 + (xc2 - xm2) * k + x2 - xm2);
float ctrl2_y = (float) (ym2 + (yc2 - ym2) * k + y2 - ym2);
return new float[]{ctrl1_x, ctrl1_y, ctrl2_x, ctrl2_y};
}
Bezier Curves are not designed to go through the provided points! They are designed to shape a smooth curve influenced by the control points.
Further you don't want to have your smooth curve going through all data points!
Instead of interpolating you should consider filtering your data set:
Filtering
For that case you need a sequence of your data, as array of points, in the order the finger has drawn the gesture:
You should look in wiki for "sliding average".
You should use a small averaging window. (try 5 - 10 points). This works as follows: (look for wiki for a more detailed description)
I use here an average window of 10 points:
start by calculation of the average of points 0 - 9, and output the result as result point 0
then calculate the average of point 1 - 10 and output, result 1
And so on.
to calculate the average between N points:
avgX = (x0+ x1 .... xn) / N
avgY = (y0+ y1 .... yn) / N
Finally you connect the resulting points with lines.
If you still need to interpolate between missing points, you should then use piece - wise cubic splines.
One cubic spline goes through all 3 provided points.
You would need to calculate a series of them.
But first try the sliding average. This is very easy.
Nice question. Your (wrong) result is obvious, but you can try to apply it to a much smaller dataset, maybe by replacing groups of close points with an average point. The appropriate distance in this case to tell if two or more points belong to the same group may be expressed in time, not space, so you'll need to store the whole touch event (x, y and timestamp). I was thinking of this because I need a way to let users draw geometric primitives (rectangles, lines and simple curves) by touch
What is this for? Why do you need to be so accurate? I would assume you only need something around 4 vertices stored for every inch the user drags his finger. With that in mind:
Try using one vertex out of every X to actually draw between, with the middle vertex used for specifying the weighted point of the curve.
int interval = 10; //how many points to skip
gesture.moveTo(gesture.get(0).x, gesture.get(0).y);
for(int i =0; i +interval/2 < gesture.size(); i+=interval)
{
Gesture ngp = gesture.get(i+interval/2);
gesturePath.quadTo(ngp.x,ngp.y, gp.x,gp.y);
}
You'll need to adjust this to actually work but the idea is there.

Translate Java 3D coordinates to 2D screen coordinates

I'm working with a Java 3D application called "Walrus" that is used to display directed graphs. The code already has a feature to highlight a node and draw label adjacent in graph given its screen coordinates.
Upon rotating the screen, the node is no more highlighted.
What I have is the node coordinates in 3D. I need to draw label to it.
Code for highlight using 3D coordinates
Point3d p = new Point3d();
m_graph.getNodeCoordinates(node, p);
PointArray array = new PointArray(1, PointArray.COORDINATES);
array.setCoordinate(0, p);
m_parameters.putModelTransform(gc);
gc.setAppearance(m_parameters.getPickAppearance());
How can I draw Label with 3D coordinates( Raster graphics throws error Renderer: Error creating immediate mode Canvas3D graphics context )
How can I convert 3D coordinates to 2D screen and use existing code to draw label at 2D screen point
Thanks,
Dakshina
I have an algorithm/method for converting [x,y,z] into [x,y] with the depth parameter:
The x value is : (int) (x - (z / depth * x))
The y value is : (int) (y - (z / depth * y))
Essentially, the depth is the focal point. The vanishing point will be at [0,0,depth].
Here's what i used to convert my 3D coordinates into perspective 2D, x2 and y2 being the 2dimensional coordinates, xyz being the 3D coordinates.
use these formulas:
x2 = cos(30)*x - cos(30)*y
y2 = sin(30)*x + sin(30)*y + z
I picked the angle 30 as it is easy for perspective purposes, also used in Isometric grids for drawing 3D on 2D papers. As the z axe will be the vertical one, x and y are the ones at 60 degrees from it right and left. Isometric Grid Picture.
I'm still working on rotation, but without altering the axes, just coordinate rotation in 3D.
Enjoy.
I found the solution.
This is the function to display Text3D at image 2D coordinates
public void drawLabel(GraphicsContext3D gc, double x, double y, int zOffset, String s) {
boolean frontBufferRenderingState = gc.getFrontBufferRendering();
gc.setBufferOverride(true);
gc.setFrontBufferRendering(true);
Point3d eye = getEye();
double labelZ = zOffset * LABEL_Z_OFFSET_SCALE
+ LABEL_Z_SCALE * eye.z + LABEL_Z_OFFSET;
double xOffset = LABEL_X_OFFSET * m_pixelToMeterScale;
double yOffset = LABEL_Y_OFFSET * m_pixelToMeterScale;
Point3d p = new Point3d(x + xOffset, y + yOffset, 0.0);
{
// Project given (x, y) coordinates to the plane z=labelZ.
// Convert from image-plate to eye coordinates.
p.x -= eye.x;
p.y -= eye.y;
double inversePerspectiveScale = 1.0 - labelZ / eye.z;
p.x *= inversePerspectiveScale;
p.y *= inversePerspectiveScale;
// Convert from eye to image-plate coordinates.
p.x += eye.x;
p.y += eye.y;
}
Transform3D scale = new Transform3D();
scale.set(LABEL_SCALE);
Vector3d t = new Vector3d(p.x, p.y, labelZ);
Transform3D translation = new Transform3D();
translation.set(t);
translation.mul(scale);
Transform3D transform = new Transform3D(m_imageToVworld);
transform.mul(translation);
gc.setModelTransform(transform);
//-----------------
int fontSize=(int)(10*m_magnification);
if(fontSize>20)
fontSize=20;
//---------------
// XXX: Courier may not be available on all systems.
Text2D text = new Text2D(s, new Color3f(1.0f, 1.0f, 1.0f),
"Courier", fontSize, Font.BOLD);
gc.draw(text);
gc.flush(true);
// NOTE: Resetting the model transform here is very important.
// For some reason, not doing this causes the immediate
// following frame to render incorrectly (but subsequent
// frames will render correctly). In some ways, this
// makes sense, because most rendering code assumes that
// GraphicsContext3D has been set to some reasonable
// transform.
gc.setModelTransform(m_objectTransform);
gc.setFrontBufferRendering(frontBufferRenderingState);
}
This is the function to take 3D coordinates and convert them to image 2D coordinates and render using above function
private boolean displayOnScreenLabel(int node, String label) {
boolean success = false;
try {
Transform3D transform = m_parameters.getObjectToEyeTransform();
Point3d nodeC = new Point3d();
m_graph.getNodeCoordinates(node, nodeC);
transform.transform(nodeC);
Point3d eye = m_parameters.getEye();
double perspectiveScale = 1.0 / (1.0 - nodeC.z / eye.z);
double centerX = eye.x + nodeC.x * perspectiveScale;
double centerY = eye.y + nodeC.y * perspectiveScale;
GraphicsContext3D gc = m_canvas.getGraphicsContext3D();
m_parameters.drawLabel(gc, centerX, centerY, m_labelZOffsetCounter++, label);
success = true;
} catch (final java.lang.OutOfMemoryError error) {
JOptionPane.showMessageDialog(m_frame, "The 3D Graphics is unable to find enough memory on your system. Kill the application!", "Out Of Memory!", JOptionPane.ERROR_MESSAGE);
} catch (Exception e) {
success = false;
}
return success;
}

Categories