Hola de nuevo amigos desarrolladores, hoy os traigo cosas muy interesantes rescatadas de mi baúl de los recuerdos.
Seguramente muchos os encontréis ante la necesidad de pintar texto siguiendo un trazado, queriendo pintar letras con un contorno hecho con figuras de formas o simplemente queriendo dibujar usando un trazo personalizado.
Pues bien todo esto está permitido en Java gracias al uso de la interfaz
Stroke y algunos truquillos que os paso a detallar.
La interfaz
Stroke permite que un objeto
Graphic2D obtenga una forma que es el contorno, o representación estética del contorno, de una
Shape (Forma) especificada.
Dibujar una
Shape es como trazar su contorno con un lápiz con la forma y tamaño apropiado.
Así pues todo nuestro trabajo consistirá en decirle a la interfaz
Stroke que
Shape tiene que devolvernos para dibujar cualquier objeto gráfico con
Graphic2D. Esto se consigue al sobreescribir el método
public Shape createStrokedShape(Shape shape) para hacer que nos devuelva la forma que deseamos utilizar como lápiz.
Las posibilidades que ofrece esta interfaz son muchas, este sería el resultado de aplicar algunos de estos lápices personalizados a la hora de dibujar una línea, un círculo y los caracteres "A B C".
Por ejemplo, vamos a crear un contorno formado por figuras geométricas, en nuestro ejemplo serán círculos y cuadrados que se repetirán a lo largo del trazado. En los comentarios del código intento explicar el funcionamiento del mismo.
package strokes.samples;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
public class StrokeGeometrico implements Stroke {
// Formas a incluir en el stroke
private Shape shapes[];
private float advance;
// Valores necesarios para dibujar el Path (trazado)
private AffineTransform t = new AffineTransform();
/**
* Crea un trazo a partir de un array de formas.
* @param shapes Formas a dibujar
* @param advance separación entre las formas
*/
public StrokeGeometrico(Shape shapes[], float advance) {
this.advance = advance;
this.shapes = new Shape[shapes.length];
for ( int i = 0; i < this.shapes.length; i++ ) {
Rectangle2D bounds = shapes[i].getBounds2D();
t.setToTranslation(-bounds.getCenterX(), -bounds.getCenterY());
this.shapes[i] = t.createTransformedShape(shapes[i]);
}
}
@Override
public Shape createStrokedShape( Shape shape ) {
/* Proporciona implementaciones del trazado geométrico en general que soporta
* la funcionalidad de las clases Shape y PathIterator
*/
GeneralPath result = new GeneralPath();
// Usaremos este iterator para 'aplanar' el trazado
PathIterator it = new FlatteningPathIterator(shape.getPathIterator( null ), 1);
float points[] = new float[6];
float moveX = 0, moveY = 0;
float lastX = 0, lastY = 0;
float thisX = 0, thisY = 0;
int type = 0;
float next = 0;
int currentShape = 0;
int length = shapes.length;
/* Todas estas operaciones son necesarias para renderizar la forma del trazado.
* La interfaz PathIterator proporciona el mecanismo para que objetos que implementan
* la interfaz Shape devuelvan la geometria de sus formas, permitiendo que quien los llama
* recupere el trazado de esa forma segmento por segmeto.
*/
while (currentShape < length && !it.isDone()) {
type = it.currentSegment(points);
switch(type){
/* Una constante que indica que en un segmento indica que un
* punto debe comenzar un nuevo trazado.
*/
case PathIterator.SEG_MOVETO:
moveX = lastX = points[0];
moveY = lastY = points[1];
result.moveTo( moveX, moveY );
next = 0;
break;
/* Una constante que indica que el anterior trazado debe ser cerrado
* añadiendo una linea al segmento uniendo el punto correspondiente
* al anterior SEG_MOVETO.
*/
case PathIterator.SEG_CLOSE:
points[0] = moveX;
points[1] = moveY;
// Fall into....
/* La constante para un punto que indica el punto final de una linea que
* se dibuja desde el punto especificado con anterioridad.
*/
case PathIterator.SEG_LINETO:
thisX = points[0];
thisY = points[1];
float dx = thisX-lastX;
float dy = thisY-lastY;
float distance = (float) Math.sqrt(dx*dx + dy*dy);
if (distance >= next) {
float r = 1.0f / distance;
float angle = (float) Math.atan2(dy, dx);
while (currentShape < length && distance >= next) {
float x = lastX + next * dx * r;
float y = lastY + next * dy * r;
t.setToTranslation( x, y );
t.rotate(angle);
result.append(t.createTransformedShape(shapes[currentShape]), false);
next += advance;
currentShape++;
currentShape %= length;
}
}
next -= distance;
lastX = thisX;
lastY = thisY;
break;
}
it.next();
}
return result;
}
}
Además podemos, por ejemplo, combinar varios
Strokes. A continuación os muestro como crear un lápiz compuesto por otros 2 creados previamente. Basta con hacer que el nuevo
Stroke devuelva una forma creada a partir de los trazados de los 2 previamente creados. Así pues, podemos crear un trazado compuesto por un trazado simple de 10px de grosor y otro de 0.5px de la siguiente manera.
package strokes.samples;
import java.awt.Shape;
import java.awt.Stroke;
public class StrokeCompuesto implements Stroke {
// Trazos básicos
private Stroke stroke1;
private Stroke stroke2;
/**
* Crea un trazo compuesto por 2 trazos.
*/
public StrokeCompuesto(Stroke stroke1, Stroke stroke2) {
this.stroke1 = stroke1;
this.stroke2 = stroke2;
}
@Override
public Shape createStrokedShape(Shape shape) {
// Simplemente creamos el contorno del trazado usando otro trazado dado.
return stroke2.createStrokedShape(stroke1.createStrokedShape(shape));
}
}
Otra cosa que me parece muy interesante y que a muchos os habrá dado quebraderos de cabeza es lo que indica el título de la entrada, dibujar texto siguiendo un camino. Los caracteres al fin y al cabo son formas también, por lo que podemos decirle a nuestra interfaz
Stroke que nos devuelva un lápiz que pinte nuestra cadena de texto de una manera similar a como hicimos con las formas geométricas.
package strokes.samples;
import java.awt.Font;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
public class StrokeTexto implements Stroke {
// Texto y fuente a utilizar
private String text;
private Font font;
// Valores necesarios para dibujar el Path (trazado)
private boolean stretchToFit = false;
private AffineTransform t = new AffineTransform();
/**
* Crearemos un trazado a partir de una cadena de texto.
* @param text
* @param font
* @param stretchToFit Indica si queremos que el texto ocupe todo el trazado
* @param repeat Indica si queremos que la cadena se repita
*/
public StrokeTexto( String text, Font font, boolean stretchToFit) {
this.text = text;
this.font = font;
this.stretchToFit = stretchToFit;
}
@Override
public Shape createStrokedShape(Shape shape) {
// Crearemos un vector con los caracteres dados, recordemos que cada uno de los caracteres (o glifos) son simples formas
FontRenderContext frc = new FontRenderContext(null, true, true);
GlyphVector glyphVector = font.createGlyphVector(frc, text);
GeneralPath result = new GeneralPath();
PathIterator it = new FlatteningPathIterator( shape.getPathIterator( null ), 1 );
float points[] = new float[6];
float moveX = 0, moveY = 0;
float lastX = 0, lastY = 0;
float thisX = 0, thisY = 0;
int type = 0;
float next = 0;
int currentChar = 0;
int length = glyphVector.getNumGlyphs();
if ( length == 0 )
return result;
// Si queremos que el texto se adapte al trazado que vamos a realizar, necesitaremos un factor que adapte las longitudes
float factor = stretchToFit ? measurePathLength(shape) / (float)glyphVector.getLogicalBounds().getWidth() : 1.0f;
float nextAdvance = 0;
/* Todas estas operaciones son necesarias para renderizar la forma del trazado.
* La interfaz PathIterator proporciona el mecanismo para que objetos que implementan
* la interfaz Shape devuelvan la geometria de sus formas, permitiendo que quien los llama
* recupere el trazado de esa forma segmento por segmeto.
*/
while (currentChar < length && !it.isDone()) {
type = it.currentSegment(points);
switch(type){
/* Una constante que indica que en un segmento indica que un
* punto debe comenzar un nuevo trazado.
*/
case PathIterator.SEG_MOVETO:
moveX = lastX = points[0];
moveY = lastY = points[1];
result.moveTo(moveX, moveY);
nextAdvance = glyphVector.getGlyphMetrics(currentChar).getAdvance() * 0.5f;
next = nextAdvance;
break;
/* Una constante que indica que el anterior trazado debe ser cerrado
* añadiendo una linea al segmento uniendo el punto correspondiente
* al anterior SEG_MOVETO.
*/
case PathIterator.SEG_CLOSE:
points[0] = moveX;
points[1] = moveY;
// Fall into....
/* La constante para un punto que indica el punto final de una linea que
* se dibuja desde el punto especificado con anterioridad.
*/
case PathIterator.SEG_LINETO:
thisX = points[0];
thisY = points[1];
float dx = thisX-lastX;
float dy = thisY-lastY;
float distance = (float)Math.sqrt( dx*dx + dy*dy );
if (distance >= next) {
float r = 1.0f/distance;
float angle = (float)Math.atan2( dy, dx );
while (currentChar < length && distance >= next) {
Shape glyph = glyphVector.getGlyphOutline( currentChar );
Point2D p = glyphVector.getGlyphPosition(currentChar);
float px = (float)p.getX();
float py = (float)p.getY();
float x = lastX + next*dx*r;
float y = lastY + next*dy*r;
float advance = nextAdvance;
nextAdvance = currentChar < length-1 ? glyphVector.getGlyphMetrics(currentChar+1).getAdvance() * 0.5f : 0;
t.setToTranslation( x, y );
t.rotate( angle );
t.translate( -px-advance, -py );
result.append( t.createTransformedShape(glyph), false );
next += (advance+nextAdvance) * factor;
currentChar++;
currentChar %= length;
}
}
next -= distance;
lastX = thisX;
lastY = thisY;
break;
}
it.next();
}
return result;
}
/**
* Mide la longitud del trazado.
* @param shape
* @return la longitud
*/
public float measurePathLength(Shape shape) {
PathIterator it = new FlatteningPathIterator(shape.getPathIterator( null ), 1);
float points[] = new float[6];
float moveX = 0, moveY = 0;
float lastX = 0, lastY = 0;
float thisX = 0, thisY = 0;
int type = 0;
float total = 0;
while (!it.isDone()) {
type = it.currentSegment( points );
switch(type){
case PathIterator.SEG_MOVETO:
moveX = lastX = points[0];
moveY = lastY = points[1];
break;
case PathIterator.SEG_CLOSE:
points[0] = moveX;
points[1] = moveY;
// Fall into....
case PathIterator.SEG_LINETO:
thisX = points[0];
thisY = points[1];
float dx = thisX-lastX;
float dy = thisY-lastY;
total += (float)Math.sqrt(dx * dx + dy * dy);
lastX = thisX;
lastY = thisY;
break;
}
it.next();
}
return total;
}
}
package strokes;
import java.awt.BasicStroke;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.GlyphVector;
import java.awt.geom.Ellipse2D;
import javax.swing.JPanel;
import strokes.samples.StrokeCompuesto;
import strokes.samples.StrokeGeometrico;
import strokes.samples.StrokeTexto;
public class StrokesSamples extends JPanel {
private StrokeCompuesto compStroke;
private StrokeGeometrico geomStroke;
private StrokeTexto textStroke;
private Font font;
/**
* Constructor.
*/
public StrokesSamples() {
// Un trazo hecho compuesto por cuadrados y circulos
geomStroke = new StrokeGeometrico(
new Shape[] {
new Rectangle(5, 5),
new Ellipse2D.Float(0, 0, 4, 4)
}, 10.0f);
// Un trazo compuesto por 2 trazos.
compStroke = new StrokeCompuesto(new BasicStroke(10f), new BasicStroke(0.5f));
// Un trazo compuesto por una cadena de caracteres
font = new Font("Arial", Font.PLAIN, 12);
textStroke = new StrokeTexto("Esto es una cadena", font, false);
}
@Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
// Activamos el anti aliasing para que nuestros trazados se rendericen en condiciones optimas
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Esto lo hacemos para obtener el contorno de la cadena "A B C"
Font font = new Font("Arial", Font.PLAIN, 100);
g2d.setFont(font);
GlyphVector gv = font.createGlyphVector(g2d.getFontRenderContext(), "A B C");
// Así podremos dibujar esta forma con nuestros lápices
Shape shape = gv.getOutline();
// Dibujamos usando nuestro trazo con formas
g2d.setStroke(geomStroke);
g2d.drawLine(5, 10, 105, 25);
g2d.drawOval(5, 30, 75, 75);
// el método translate desplaza el origen de coordenadas al punto específicado
g2d.translate(105, 75);
g2d.draw(shape);
g2d.translate(-105, -75);
// Dibujamos usando el trazado compuesto
g2d.setStroke(compStroke);
g2d.drawLine(5, 120, 105, 135);
g2d.drawOval(5, 140, 75, 75);
g2d.translate(105, 185);
g2d.draw(shape);
g2d.translate(-105, -185);
// Dibujamos usando el trazo de cadenas de texto
g2d.setStroke(textStroke);
g2d.drawLine(5, 230, 105, 245);
g2d.drawOval(5, 250, 75, 75);
g2d.translate(105, 295);
g2d.draw(shape);
g2d.translate(-105, -295);
};
}
Bien, solo queda dejaros el método Main que lanza nuestra aplicación de ejemplo. Un saludo y gracias a todos por leerme.
package strokes;
import javax.swing.JFrame;
public class Main {
private static void createAndShowGUI() {
JFrame frame = new JFrame("Ejemplos de uso de strokes");
StrokesSamples ejemplos = new StrokesSamples();
frame.getContentPane().add(ejemplos);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 400);
frame.setVisible(true);
}
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
}