Java - Does subpixel line accuracy require an AffineTransform? - java

I've never worked with Java drawing methods before, so I decided to dive in and create an analog clock as a PoC. In addition to the hands, I draw a clock face that includes tick marks for minutes/hours. I use simple sin/cos calculations to determine the position of the lines around the circle.
However, I've noticed that since the minute tick-marks are very short, the angle of the lines looks wrong. I'm certain this is because both Graphics2D.drawLine() and Line2D.double() methods cannot draw with subpixel accuracy.
I know I can draw lines originating from the center and masking it out with a circle (to create longer, more accurate lines), but that seems like such an inelegant and costly solution. I've done some research on how to do this, but the best answer I've come across is to use an AffineTransform. I assume I could use an AffineTransform with rotation only, as opposed to having to perform a supersampling.
Is this the only/best method of drawing with sub-pixel accuracy? Or is there a potentially faster solution?
Edit: I am already setting a RenderingHint to the Graphics2D object.
As requested, here is a little bit of the code (not fully optimized as this was just a PoC):
diameter = Math.max(Math.min(pnlOuter.getSize().getWidth(),
pnlOuter.getSize().getHeight()) - 2, MIN_DIAMETER);
for (double radTick = 0d; radTick < 360d; radTick += 6d) {
g2d.draw(new Line2D.Double(
(diameter / 2) + (Math.cos(Math.toRadians(radTick))) * diameter / 2.1d,
(diameter / 2) + (Math.sin(Math.toRadians(radTick))) * diameter / 2.1d,
(diameter / 2) + (Math.cos(Math.toRadians(radTick))) * diameter / 2.05d,
(diameter / 2) + (Math.sin(Math.toRadians(radTick))) * diameter / 2.05d));
} // End for(radTick)
Here's a screenshot of the drawing. It may be somewhat difficult to see, but look at the tick mark for 59 minutes. It is perfectly vertical.

Line2D.double() methods cannot draw
with subpixel accuracy.
Wrong, using RenderingHints.VALUE_STROKE_PURE the Graphics2D object can draw "subpixel" accuracy with the shape Line2D.
I assume I could use an
AffineTransform with rotation only, as
opposed to having to perform a
supersampling. Is this the only/best
method of drawing with sub-pixel
accuracy? Or is there a potentially
faster solution?
I think you are missing somthing here. The Graphics2D object already holds a AffineTransform and it is using it for all drawing actions and its cheap performance wise.
But to get back to you what is missing from your code - this is missing:
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
Below is a self contained example that produces this picture:
public static void main(String[] args) throws Exception {
final JFrame frame = new JFrame("Test");
frame.add(new JComponent() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
System.out.println(g2d.getTransform());
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
double dia = Math.min(getWidth(), getHeight()) - 2;
for (int i = 0; i < 60 ; i++) {
double angle = 2 * Math.PI * i / 60;
g2d.draw(new Line2D.Double(
(dia / 2) + Math.cos(angle) * dia / 2.1d,
(dia / 2) + Math.sin(angle) * dia / 2.1d,
(dia / 2) + Math.cos(angle) * dia / 2.05d,
(dia / 2) + Math.sin(angle) * dia / 2.05d));
}
g2d.draw(new Ellipse2D.Double(1, 1, dia - 1, dia - 1));
}
});
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 400);
frame.setVisible(true);
}

Related

Draw text in circle with Java AWT (with the letters oriented accordingly)

I'm trying to use Java AWT with AffineTranform to draw a given String in a circle, where the letters would also go upside down along the circle.
I started with the code from the following program to draw text alone a curve.
I've also used the calculation of the coordinates from a snippet I found here for drawing the numbers of an analog clock.
Below is my code. To be completely honest, I don't understand 100% how these methods work in order to fix my code. I've been fiddling around a bit in a trial-and-error attempt with the coords and theta values.
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Panel;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
public class Main extends Panel {
public static void main(String[] args){
Frame f = new Frame("Circle Text");
f.add(new Main());
f.setSize(750, 750);
f.setVisible(true);
}
private int[] getPointXY(int dist, double rad){
int[] coord = new int[2];
coord[0] = (int) (dist * Math.cos(rad) + dist);
coord[1] = (int) (-dist * Math.sin(rad) + dist);
return coord;
}
#Override
public void paint(Graphics g){
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// Hard-coded for now, using 12 characters for 30 degrees angles (like a clock)
String text = "0123456789AB";
Font font = new Font("Serif", 0, 25);
FontRenderContext frc = g2.getFontRenderContext();
g2.translate(200, 200); // Starting position of the text
GlyphVector gv = font.createGlyphVector(frc, text);
int length = gv.getNumGlyphs(); // Same as text.length()
final double toRad = Math.PI / 180;
for(int i = 0; i < length; i++){
//Point2D p = gv.getGlyphPosition(i);
int[] coords = getPointXY(100, -360 / length * i * toRad + Math.PI / 2);
double theta = 2 * Math.PI / (length + 1) * i;
AffineTransform at = AffineTransform.getTranslateInstance(coords[0], coords[1]);
at.rotate(theta);
Shape glyph = gv.getGlyphOutline(i);
Shape transformedGlyph = at.createTransformedShape(glyph);
g2.fill(transformedGlyph);
}
}
}
And this is the current output:
I also noticed that if I use (2 * length) instead of (length + 1) in the theta formula, the first halve of the string seems to be in the correct positions, except not angled properly oriented (the character '6' is sideways / 90 degrees rotated, instead of upside down / 180 degrees rotated):
As I mentioned, I don't really know how the AffineTransform works regarding the given coordinates and theta. An explanation of that would be greatly appreciated, and even more so if someone could help me fix the code.
Also note that I want to implement this formula for a variable length of the String. I've now hard-coded it to "0123456789AB" (12 characters, so it's similar to a clock with 30 degrees steps), but it should also work with let's say a String of 8 characters or 66 characters.
EDIT: After the suggestions of #MBo I made the following modifications to the code:
int r = 50;
int[] coords = getPointXY(r, -360 / length * i * toRad + Math.PI / 2);
gv.setGlyphPosition(i, new Point(coords[0], coords[1]));
final AffineTransform at = AffineTransform.getTranslateInstance(0, 0);
at.rotate(-2 * Math.PI * i / length);
at.translate(r * Math.cos(Math.PI / 2 - 2 * Math.PI * i / length),
r * Math.sin(Math.PI / 2 - 2 * Math.PI * i / length));
Shape glyph = gv.getGlyphOutline(i);
Shape transformedGlyph = at.createTransformedShape(glyph);
g2.fill(transformedGlyph);
I now do have a circle, so that's something, but unfortunately still with three issues:
The starting position of the first character is at ~4 o'clock instead of top.
The characters aren't correctly angled with their tops towards the center of the circle
The string is drawn counterclockwise instead of clockwise
The last issue is easily fixed, by changing the -2 to 2 in the rotate:
But the other two?
EDIT2: I misread a small section of #MBo's answer regarding the initial glyph set. It's now working. Here the resulting code changes again in comparison to the Edit above:
gv.setGlyphPosition(i, new Point(-length / 2, -length / 2));
AffineTransform at = AffineTransform.getTranslateInstance(coords[0], coords[1]);
at.rotate(2 * Math.PI * i / length);
Although I still see some minor issues with larger input Strings, so will look into that.
EDIT3: It's been a while, but I just got back to this, and I spotted my mistake for the length 66 test case I tried pretty easily: 360 should be a 360d, because the 360/length would use integer-division otherwise if 360 isn't evenly divisible by the length.
I now have this, which works as intended for any length. Note that the center isn't completely correct, for which the answer provided by #Mbo can help. My only goal was to make the circle of text (of length 66). Where it is on the screen and how big wasn't really that important.
int[] coords = this.getPointXY(r, -360.0 / length * i * toRad + Math.PI / 2);
gv.setGlyphPosition(i, new Point(0, 0));
AffineTransform at = AffineTransform.getTranslateInstance(coords[0], coords[1]);
at.rotate(2 * Math.PI * i / length);
at.translate(r * Math.cos(Math.PI / 2 - 2 * Math.PI * i / length),
r * Math.sin(Math.PI / 2 - 2 * Math.PI * i / length));
at.translate(-FONT_SIZE / 2, 0);
Your initial angle is Pi/2 for position and 0 for glyph rotation.
To set rotation and position properly, I suggest:
put glyph in the coordinate origin (0,0)
rotate it by -2*Math.PI * i / length
translate it by r*cos(Math.PI/2 - 2*Math.PI * i / length) and r*sin(Math.PI/2 - 2*Math.PI * i / length)
translate it by circle center coordinates
Steps:
Note - rotate, then shift.
This approach perhaps give good but not perfect result. For better looking you can add the first step - translate glyph by half of it's size to provide rotation about it's center. So sequence:
shift by -glyphpixelsize/2
rotate
shift into final position (relative to zero, then shift by circle center)

Swing translate scale change order misplacement

I've found something weird when splitting a translate operation around a scaling one with Java Swing. Maybe I'm doing something stupid but I'm not sure where.
In the first version I center the image, scale it and then translate it to the desired position.
In the second version I directly scale the image and then translate to the desired position compensating for having a non centered image.
The two solutions should be equivalent. Also this is important when considering rotations around a point and motion in another.. I've code that does that too... but why this does not work?
Here are the two versions of the code. They are supposed to do the exact same thing but they are not. Here are the screenshots:
First produces: screenshot1
Second produces: screenshot2
I think that the two translation operations in draw1 surrounding the scale operation should be equivalent to the scale translate operation in draw2.
Any suggestion?
MCVE:
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.net.URL;
public class Asteroid extends JComponent implements ActionListener {
public static final Dimension FRAME_SIZE = new Dimension(640, 480);
public double x = 200;
public double y = 200;
public int radius = 40;
private AffineTransform bgTransfo;
private final BufferedImage im2;
private JCheckBox draw1Check = new JCheckBox("Draw 1", true);
Asteroid() {
BufferedImage img = null;
try {
img = ImageIO.read(new URL("https://i.stack.imgur.com/CWJdo.png"));
} catch (Exception e) {
e.printStackTrace();
}
im2 = img;
initUI();
}
private final void initUI() {
draw1Check.addActionListener(this);
JFrame frame = new JFrame("FrameDemo");
frame.add(BorderLayout.CENTER, this);
frame.add(BorderLayout.PAGE_START, draw1Check);
frame.pack();
frame.setVisible(true);
frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
Asteroid asteroid = new Asteroid();
}
#Override
public Dimension getPreferredSize() {
return FRAME_SIZE;
}
#Override
public void paintComponent(Graphics g0) {
Graphics2D g = (Graphics2D) g0;
g.setColor(Color.white);
g.fillRect(0, 0, 640, 480);
if (draw1Check.isSelected()) {
draw1(g);
} else {
draw2(g);
}
}
public void draw1(Graphics2D g) {//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
bgTransfo = new AffineTransform();
//centering
bgTransfo.translate(-imWidth / 2.0, -imHeight / 2.0);
//scaling
bgTransfo.scale(stretchx, stretchy);
//translation
bgTransfo.translate(x / stretchx, y / stretchy);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
public void draw2(Graphics2D g) {//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
bgTransfo = new AffineTransform();
//scale
bgTransfo.scale(stretchx, stretchy);
//translate and center
bgTransfo.translate((x - radius) / stretchx, (y - radius) / stretchy);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
#Override
public void actionPerformed(ActionEvent e) {
repaint();
}
}
Not sure if this question is still really open. Anyway here is my answer.
I think the crucial part to understand this behavior is the difference between AffineTransform.concatenate and AffineTransform.preConcatenate methods. The thing is that resulting transformation depends on the order the sub-transformations are applied.
To quote the concatenate JavaDoc
Concatenates an AffineTransform Tx to this AffineTransform Cx in the most commonly useful way to provide a new user space that is mapped to the former user space by Tx. Cx is updated to perform the combined transformation. Transforming a point p by the updated transform Cx' is equivalent to first transforming p by Tx and then transforming the result by the original transform Cx like this: Cx'(p) = Cx(Tx(p))
compare this with preConcatenate:
Concatenates an AffineTransform Tx to this AffineTransform Cx in a less commonly used way such that Tx modifies the coordinate transformation relative to the absolute pixel space rather than relative to the existing user space. Cx is updated to perform the combined transformation. Transforming a point p by the updated transform Cx' is equivalent to first transforming p by the original transform Cx and then transforming the result by Tx like this: Cx'(p) = Tx(Cx(p))
The scale and translate methods are effectively concatenate. Lets call 3 transformations in your draw1 method C (center), S (scale), and T (translate). So your compound transformation is effectively C(S(T(p))). Particularly it means that S is applied to the T but not to the C so your C does not really center the image. A simple fix would be to change the order of S and C but I think that a more proper fix would be something like this:
public void draw3(Graphics2D g) {
//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
AffineTransform bgTransfo = new AffineTransform();
//translation
bgTransfo.translate(x, y);
//scaling
bgTransfo.scale(stretchx, stretchy);
//centering
bgTransfo.translate(-imWidth / 2.0, -imHeight / 2.0);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
I think the big advantage of this method is that you don't have to re-calculate the T using stretchx/stretchy

How do I increase the frequency JPanel is redrawn?

I have a simple program that draws the trajectory of a particle launched from the origin at a certain speed and angle. I created a subclass of JPanel to handle the drawing of this. My everytime my subclass is redrawn it takes the difference between the current time and the initial time(both in milliseconds), converts this to seconds, then finds the x and y coordinate of where the particle should be at that point in time, and finally takes those x and y coordinates and draws them on the screen. My problem is that my subclass seems to be redrawn at interval that seem long because there are only a few dots that are shown.
My drawing method:
private void doDrawing(Graphics g) {
Dimension size = getSize();
Insets insets = getInsets();
int w = size.width - insets.left - insets.right;
int h = size.height - insets.top - insets.bottom;
Graphics2D g2d = (Graphics2D) g;
g.drawString("Acceleration: -9.8m/s i", 0, 20);
StringBuilder b = new StringBuilder();
b.append("Current Velocity: ");
b.append(String.valueOf(sim.getVector(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000)).getMagnitude()));
b.append(" m/s at ");
b.append(String.valueOf(sim.getVector(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000)).getDirection().getDirectionDeg()));
b.append(" degrees");
g.drawString(b.toString(), 0, 30);
drawPreviousPoints(g2d);
drawCurrentPointAndAppend(g2d, w, h);
repaint();
}
private void drawCurrentPointAndAppend(Graphics2D g2d, int w, int h) {
g2d.setColor(Color.red);
double height = (length / w) * h;
Vector2D c = sim.getVector(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000));
double currentX = w
* ((sim.getX(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000))) / length);
double currentY = h
* (1 - ((sim.getY(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000))) / height));
g2d.drawLine((int) currentX, (int) currentY, (int) currentX,
(int) currentY);
g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE,
BasicStroke.JOIN_MITER));
g2d.drawLine((int) currentX, (int) (currentY),
(int) (currentX + w * (c.getX() / length)),
(int) (currentY + (h * -(c.getY() / height))));
xList.add(currentX);
yList.add(currentY);
}
private void drawPreviousPoints(Graphics2D g2d) {
g2d.setColor(Color.blue);
g2d.setStroke(new BasicStroke(7, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND));
if (!xList.isEmpty()) {
for (int i = 0; i < xList.size(); i++) {
g2d.drawLine(xList.get(i).intValue(), yList.get(i).intValue(),
xList.get(i).intValue(), yList.get(i).intValue());
}
}
}
tickSpeed is just a variable that I use to speed up or slow down the particle. It runs fine; however, the animation seems very choppy.
How do I fix this choppiness(make everything seem more "fluid")
Where should I call repaint()? Because I feel like calling it at the end of my drawing method isn't right.
An important rule of Swing- You don't control the paint process...
Don't perform these calculations within the paintComponent. The paintComponent is meant to paint the current state of the UI and may be called at any time for many reasons, most of which are outside your control.
Instead, consider using a javax.swing.Timer set to repeat at a regular interval (40ms is 25 ticks a second).
Set up a model which keeps track of the particles current been processed. When the timer ticks, calculate your particle positions and update them, then call repaint.
Within your paintComponent, simply paint the current state of your model.
Have a look at Concurrency in Swing and How to use Swing Timers for more details
The paint process is internally handled so you can not control the frequency of it's execution.
However, you can create separate threads or timers which can invoke processes at your desired frequency. Use the paint method only to render on your canvas, do other logic and processing in another function.

JFrame duplicates drawings when resizing

I wrote a code in Java (using swing) which draws few polygons on a panel.
public MyClass extends JPanel
The code is very simple (but long) and basically adds few Polygons, then adds few points to each polygon and then draw them on the screen (with drawPolygon).
My problem is when I run the program, I can't see the drawings on the panel.
After a while, I figure out that when I re-size my frame, I can suddenly see the drawing but it duplicates itself many times (depends how much I re-size). If I play enough time with the resizing, I get:
java.lang.OutOfMemoryError: Java heap space
Also, myPolygon.invalidate() doesn't help.
When using setResizable(false) I can't see my drawing at all.
Does anyone have a solution?
Duplicate Image Screenshot:1
To start with, in your paintComponent method, don't call
setPreferredSize(new Dimension(500,500));
setVisible(true);
validate();
This will request a repaint, cause paintComponent to be recalled and you'll end up in a nasty loop, consuming your CPU and (as you have found out), your memory.
IF you can get away with it, you're better off to draw the polygon to a buffer and draw the buffer to the screen on each iteration of the paintComponent. This will be faster in the long run...
// Create a field
private BufferedImage buffer;
// Call this when you need to change the polygon some how...
protected void createBuffer() {
// You need to determine the width and height values ;)
buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
int xoffset=5;//Multiply in order to "zoom" the picture
int offset=0;//moves shape to the right
p.addPoint(40*xoffset-offset, 30*xoffset-offset);
p.addPoint(50*xoffset-offset,30*xoffset-offset);
p.addPoint(57*xoffset-offset,37*xoffset-offset);
p.addPoint(57*xoffset-offset,47*xoffset-offset);
p.addPoint(50*xoffset-offset,54*xoffset-offset);
p.addPoint(40*xoffset-offset,54*xoffset-offset);
p.addPoint(33*xoffset-offset,47*xoffset-offset);
p.addPoint(33*xoffset-offset, 37*xoffset-offset);
g.drawPolygon(p);
g.dispose();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (buffer != null) {
Graphics2D g2d = (Graphics2D)g;
g2d.drawImage(buffer, translateX, translateY, this);
}
}
UPDATE
So, anyway, the other fun things you're doing are...
Creating a static reference to your Polygon. Hope you weren't intending to have more the one on the screen at a time ;)
Add new points to an already existing polygon (each time paintComponent was called)
Translating the polygon each time paintComponent was called
Try something like this instead
public class RoundTop extends JPanel {
//Polygons declarations
private Polygon p = new Polygon();
//Translate variables;
private int translateX = 10;
private int translateY = 10;
public RoundTop() {
int xoffset = 5;//Multiply in order to "zoom" the picture
int offset = 0;//moves shape to the right
p.addPoint(40 * xoffset - offset, 30 * xoffset - offset);
p.addPoint(50 * xoffset - offset, 30 * xoffset - offset);
p.addPoint(57 * xoffset - offset, 37 * xoffset - offset);
p.addPoint(57 * xoffset - offset, 47 * xoffset - offset);
p.addPoint(50 * xoffset - offset, 54 * xoffset - offset);
p.addPoint(40 * xoffset - offset, 54 * xoffset - offset);
p.addPoint(33 * xoffset - offset, 47 * xoffset - offset);
p.addPoint(33 * xoffset - offset, 37 * xoffset - offset);
p.translate(translateX, translateY);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.drawPolygon(p);
g2d.dispose();
}
}

Java - Draw a ruler (line with tick marks at 90 degree angle)

I'm using Java AWT to draw lines on a panel (Line2D and Graphics2D.drawLine()) and I'm wondering how I can draw a line with tick marks, similar to:
|----|----|----|----|----|
I know the positions I'd like to draw the ticks at in advance.
The lines could be in any position, so the ticks must be drawn at an angle releative to the line itself.
My basic geometry & ability to apply it in Java is failing me. :)
I suggest you
implement a ruler-drawing-method that draws a simple horizontal ruler from left to right
Figure out the desired angle using Math.atan2.
Apply an AffineTransform with translation and rotation before invoking the ruler-drawing-method.
Here is a complete test-program. (The Graphics.create method is used to create a copy of the original graphics object, so we don't mess up the original transform.)
import java.awt.*;
public class RulerExample {
public static void main(String args[]) {
JFrame f = new JFrame();
f.add(new JComponent() {
private final double TICK_DIST = 20;
void drawRuler(Graphics g1, int x1, int y1, int x2, int y2) {
Graphics2D g = (Graphics2D) g1.create();
double dx = x2 - x1, dy = y2 - y1;
double len = Math.sqrt(dx*dx + dy*dy);
AffineTransform at = AffineTransform.getTranslateInstance(x1, y1);
at.concatenate(AffineTransform.getRotateInstance(Math.atan2(dy, dx)));
g.transform(at);
// Draw horizontal ruler starting in (0, 0)
g.drawLine(0, 0, (int) len, 0);
for (double i = 0; i < len; i += TICK_DIST)
g.drawLine((int) i, -3, (int) i, 3);
}
public void paintComponent(Graphics g) {
drawRuler(g, 10, 30, 300, 150);
drawRuler(g, 300, 150, 100, 100);
drawRuler(g, 100, 100, 120, 350);
drawRuler(g, 50, 350, 350, 50);
}
});
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(400, 400);
f.setVisible(true);
}
}
Note, that you could just as easily draw numbers above the ticks. The drawString-calls would go through the same transformation and get nicely "tilted" along the line.
Things that need noting:
A perpendicular line has a slope of -1/oldslope.
In order to support lines in any direction, you need to do it parametrically
Thus, you have dy and dx across the original line, which means that newdx=dy; newdy=-1*dx.
If you have it such that <dx, dy> is a unit vector (sqrt(dx*dx+dy+dy)==1, or dx==cos(theta); dy=sin(theta) for some theta), you then just need to know how far apart you want the tick marks.
sx, sy are your start x and y
length is the length of the line
seglength is the length of the dashes
dx, dy is the slopes of the original line
newdx, newdy are the (calculated above) slopes of the cross lines
Thus,
Draw a line from <sx,sy> (start x,y) to <sx+dx*length,sy+dy*length>
Draw a set of lines (for(i=0;i<=length;i+=interval) from <sx+dx*i-newdx*seglength/2,sy+dy*i-newdy*seglength/2> to <sx+dx*i+newdx*seglength/2,sy+dy*i+newdy*seglength/2>
I hope you know matrix multiplication. In order to rotate a line you need to multiple it by rotation matrix. (I coudln't draw a proper matrix but assume both line are not separated)
|x'| = |cos(an) -sin(an)| |x|
|y`| = |sin(an) cos(an)| |y|
The old points are x,y and the new is x',y'. Let us illustrate by an example, lets say you have a vertical line from (0,0) to (0,1), now you want to rotate it by 90 degrees. (0,0) will remain zero so lets just see what happens to (0,1)
|x'| = |cos(90) -sin(90)| |0|
|y`| = |sin(90) cos(90)| |1|
==
|1 0| |0|
|0 1| |1|
==
| 1*0 + 0*1|
| 0*0 + 1*1|
== |0|
|1|
you get to horizontal line (0,0),(0,1) like you would expect.
Hope it helps,
Roni

Categories

Resources