Related
I am looking to draw outlined text around the radius of a circle, both at the top and the bottom, with both sections of text facing the correct way.
How to draw an outline around text in AWT? and Write text along a curve in Java have helped me to create code (below) that will draw text character-by-character, where each character is rotated independantly (with a few minor graphical glitches that will be the subject of a different question).
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
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.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class CreateTextStackOverflow {
private static final int EDGE_GAP = 10;
public static void main(String[] args) throws IOException {
BufferedImage bi = new BufferedImage(364,364,BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D)bi.getGraphics();
Font font = new Font("Arial",Font.BOLD,48);
FontMetrics fm = g.getFontMetrics(font);
int imageWidth = bi.getWidth();
int imageHeight = bi.getHeight();
int centerX = imageWidth/2;
int centerY = imageHeight/2;
int radius = Math.min(centerX,centerY)-EDGE_GAP-((int)Math.ceil(fm.getMaxCharBounds(g).getHeight()/2.0));
drawOutlinedTextAroundCircle(g,"TOP TEXT",0,false,imageWidth,imageHeight,radius,centerX,centerY,font,Color.BLACK,Color.WHITE);
drawOutlinedTextAroundCircle(g,"BOTTOM TEXT",0,true,imageWidth,imageHeight,radius,centerX,centerY,font,Color.RED,Color.WHITE);
ImageIO.write(bi, "png", new File("test.png"));
}
private static void drawOutlinedTextAroundCircle(Graphics2D g, String text, int textCenterAngleDegrees, boolean atBottom, int imageWidth, int imageHeight, int radius, int centerX, int centerY, Font font, Color outlineColour, Color innerColour) {
FontMetrics fm = g.getFontMetrics(font);
char[] characters = text.toCharArray();
int characterCount = characters.length;
int spaceCharacterWidth = fm.charWidth('n');
boolean[] spaces = new boolean[characterCount];
int[] characterWidths = new int[characterCount+1];
characterWidths[characterCount] = 0;
for (int index=0; index<characterCount; index++) {
char character = characters[index];
spaces[index] = character == ' ' || Character.isSpaceChar(character);
characterWidths[index] = spaces[index]?spaceCharacterWidth:fm.charWidth(character);
}
double currentAngle = 0;
double[] characterAngles = new double[characterCount];
int leading = fm.getLeading();
for (int index=0; index<characterCount; index++) {
characterAngles[index] = currentAngle;
currentAngle += Math.sin(((characterWidths[index]/2.0) + leading + (characterWidths[index+1]/2.0)) / (double)radius);
}
double adjustment = (textCenterAngleDegrees * Math.PI / 180) - ((characterAngles[characterCount-1] - characterAngles[0]) / 2.0);
for (int index=0; index<characterCount; index++) {
characterAngles[index] += adjustment;
}
AffineTransform flipTransform = new AffineTransform();
if (atBottom) {
flipTransform.scale(1,-1);
flipTransform.translate(0,-imageHeight);
}
AffineTransform translateTransform = new AffineTransform(flipTransform);
translateTransform.translate(centerX, centerY);
for (int index=0; index<characterCount; index++) {
if (!spaces[index]) {
AffineTransform rotateTransform = new AffineTransform(translateTransform);
rotateTransform.rotate(characterAngles[index], 0.0, 0.0);
float x = (float)(-(characterWidths[index]/2.0));
float y = (float)(-radius);
FontRenderContext frc = g.getFontRenderContext();
String str = new String(characters,index,1);
GlyphVector glyphVector = font.createGlyphVector(frc, str);
Rectangle2D box = glyphVector.getVisualBounds();
g.setTransform(rotateTransform);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Shape shape = glyphVector.getOutline((float)(x - box.getX()), (float)(y - box.getY()));
g.setColor(innerColour);
g.fill(shape);
g.setColor(outlineColour);
g.setStroke(new BasicStroke(1f));
g.draw(shape);
}
}
}
}
(which produces)
This has the problem where the text at the bottom is not the correct way around.
I have made several attempts at using different affine transforms to change each character as it is drawn, but to no avail.
What I am aiming for is a solution where 0 degrees results in text centered at the top/bottom of the circle, and increasing the number of degrees moves the text further to the right, as shown in this diagram mocked up in GIMP:
What is the correct way to draw text in this manner?
Starting from scratch using the code that I had written and post to the original question as a base, and also countless sheets of paper, I worked out for this to work:
If text is at the bottom, and increasing angles moves the text to the left (different from what was originally asked)
Do sequentially:
Put the characters in reverse order
Rotate the image 180 degrees
Flip each glyph horizontally and vertically
If text is at the bottom, and increasing angles moves the text to the right
Either:
Do the above, but negate the angle first
Do sequentially:
Flip the entire image horizontally
Rotate the image 180 degrees
Flip each glyph vertically
The below solution implements this (using the left option). It also separates the logic of text styling from text positioning:
package uk.co.scottdennison.java.testbed;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
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.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import uk.co.scottdennison.java.testbed.TextUtilities.CircularTextPosition;
import uk.co.scottdennison.java.testbed.TextUtilities.GlyphDrawer;
import static uk.co.scottdennison.java.testbed.TextUtilities.CircularTextPosition.BASELINE_ON_CIRCLE;
import static uk.co.scottdennison.java.testbed.TextUtilities.CircularTextPosition.INSIDE_CIRCLE;
import static uk.co.scottdennison.java.testbed.TextUtilities.CircularTextPosition.OUTSIDE_CIRCLE;
public class CreateText4 {
private static final int IMAGE_SIZE = 364;
private static final Color BACKGROUND_COLOUR = new Color(242,247,254);
private static final Font FONT = new Font("Arial",Font.BOLD,48);
private static final Stroke STROKE = new BasicStroke(1f);
private static final Color UPPER_TEXT_FILL_COLOUR = Color.WHITE;
private static final Color UPPER_TEXT_OUTLINE_COLOUR = Color.RED;
private static final Color LOWER_TEXT_FILL_COLOUR = Color.WHITE;
private static final Color LOWER_TEXT_OUTLINE_COLOUR = Color.BLUE;
public static void main(String[] args) throws IOException {
for (CircularTextPosition textPosition : CircularTextPosition.values()) {
for (int squash=0; squash<=1; squash++) {
for (int angle=0; angle<360; angle+=15) {
int edgeGap = 0;
switch (textPosition) { // For the demo, the edge gaps need to be different, but the enum in TextUtilities could be in a completely separate library so should have no knowledge of this.
case INSIDE_CIRCLE:
edgeGap = 10;
break;
case BASELINE_ON_CIRCLE:
edgeGap = 40;
break;
case OUTSIDE_CIRCLE:
edgeGap = 70;
break;
}
draw(angle,edgeGap,squash==1,textPosition);
}
}
}
}
public static void draw(int angle, int edgeGap, boolean squash, CircularTextPosition circularTextPosition) throws IOException {
System.out.println("angle=" + angle + " / squash=" + squash + " / circularTextPosition=" + circularTextPosition);
BufferedImage bi = new BufferedImage(IMAGE_SIZE,IMAGE_SIZE,BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D)bi.getGraphics();
double center = IMAGE_SIZE/2.0;
double radius = center-edgeGap;
int flooredCenter = (int)Math.floor(center);
int ceiledCenter = (int)Math.ceil(center);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(BACKGROUND_COLOUR);
g.fillRect(-1,-1,IMAGE_SIZE+2,IMAGE_SIZE+2);
g.setStroke(new BasicStroke(1f));
g.setColor(Color.GREEN);
g.drawLine(0, 0, IMAGE_SIZE, IMAGE_SIZE);
g.drawLine(flooredCenter, 0, flooredCenter, IMAGE_SIZE);
if (flooredCenter != ceiledCenter) {
g.drawLine(flooredCenter-1, 0, flooredCenter-1, IMAGE_SIZE);
}
g.drawLine(IMAGE_SIZE, 0, 0, IMAGE_SIZE);
g.drawLine(0, flooredCenter, IMAGE_SIZE, flooredCenter);
if (flooredCenter != ceiledCenter) {
g.drawLine(0, flooredCenter-1, IMAGE_SIZE, flooredCenter-1);
}
g.setColor(Color.RED);
g.drawOval((int)(center-radius),(int)(center-radius),(int)(radius+radius-1),(int)(radius+radius-1));
TextUtilities.drawTextAroundCircle(g,"Top y Text" ,angle,squash,circularTextPosition,false,radius,center,center,FONT,new OutlinedFilledGlyphDrawer(UPPER_TEXT_FILL_COLOUR,UPPER_TEXT_OUTLINE_COLOUR,STROKE));
TextUtilities.drawTextAroundCircle(g,"Bottom y Text",angle,squash,circularTextPosition,true ,radius,center,center,FONT,new OutlinedFilledGlyphDrawer(LOWER_TEXT_FILL_COLOUR,LOWER_TEXT_OUTLINE_COLOUR,STROKE));
ImageIO.write(bi, "png", new File(String.format("test_%s_%s_%03d.png", circularTextPosition.name().toLowerCase().replace("_","-"),squash?"squashed":"normal",angle)));
}
public static class OutlinedFilledGlyphDrawer implements GlyphDrawer {
private Color oldGraphicsStateColor;
private Stroke oldGraphicsStateStroke;
private final Color fillColour;
private final Color outlineColour;
private final Stroke stroke;
public OutlinedFilledGlyphDrawer(Color fillColour, Color outlineColour, Stroke stroke) {
this.fillColour = fillColour;
this.outlineColour = outlineColour;
this.stroke = stroke;
}
#Override
public void saveGraphicsStateBeforeDraw(Graphics2D g) {
this.oldGraphicsStateColor = g.getColor();
this.oldGraphicsStateStroke = g.getStroke();
}
#Override
public void drawGlyph(Graphics2D preTransformedG, Shape glyph) {
preTransformedG.setColor(this.fillColour);
preTransformedG.fill(glyph);
preTransformedG.setColor(this.outlineColour);
preTransformedG.setStroke(this.stroke);
preTransformedG.draw(glyph);
}
#Override
public void restoreGraphicsStateAfterDraw(Graphics2D g) {
g.setColor(this.oldGraphicsStateColor);
g.setStroke(this.oldGraphicsStateStroke);
}
}
}
class TextUtilities { // Ideally separate into it's own package rather and then don't use static inner classes, but for this single-file demo, it works.
public static enum CircularTextPosition {
OUTSIDE_CIRCLE {
#Override
double calculateActualRadius(double radius, double maxAscent, double maxDescent, boolean flip) {
return radius;
}
},
BASELINE_ON_CIRCLE {
#Override
double calculateActualRadius(double radius, double maxAscent, double maxDescent, boolean flip) {
return radius-(flip?maxAscent:maxDescent);
}
},
INSIDE_CIRCLE {
#Override
double calculateActualRadius(double radius, double maxAscent, double maxDescent, boolean flip) {
return radius-maxAscent-maxDescent;
}
};
abstract double calculateActualRadius(double radius, double maxAscent, double maxDescent, boolean flip);
}
public static interface GlyphDrawer {
void saveGraphicsStateBeforeDraw(Graphics2D g);
void drawGlyph(Graphics2D preTransformedG, Shape glyph);
void restoreGraphicsStateAfterDraw(Graphics2D g);
}
public static void drawTextAroundCircle(Graphics2D g, String text, int centerAngleInDegrees, boolean squash, CircularTextPosition circularTextPosition, boolean flip, double radius, double centerX, double centerY, Font font, GlyphDrawer glyphDrawer) {
AffineTransform oldTransform = g.getTransform();
glyphDrawer.saveGraphicsStateBeforeDraw(g);
FontMetrics fm = g.getFontMetrics(font);
FontRenderContext frc = g.getFontRenderContext();
char[] characters = text.toCharArray();
int characterCount = characters.length;
if (flip) {
char[] reversedCharacters = new char[characterCount];
for (int index=0; index<characterCount; index++) {
reversedCharacters[index] = characters[characterCount-index-1];
}
characters = reversedCharacters;
}
double maxAscent;
double maxDescent;
if (squash) {
maxAscent = 0;
maxDescent = 0;
} else {
maxAscent = fm.getMaxAscent();
maxDescent = fm.getMaxDescent();
}
double spaceCharacterWidth = fm.charWidth('n');
double leading = fm.getLeading();
boolean[] charactersAreSpaces = new boolean[characterCount];
GlyphVector[] characterGlyphVectors = new GlyphVector[characterCount];
Rectangle2D[] characterGlyphBounds = new Rectangle2D[characterCount];
double[] characterWidths = new double[characterCount+1];
double[] characterAscents = new double[characterCount];
double[] characterDescents = new double[characterCount];
for (int index=0; index<characterCount; index++) {
char character = characters[index];
boolean isSpace = character == ' ' || Character.isSpaceChar(character);
GlyphVector glyphVector = font.createGlyphVector(frc, Character.toString(character));
Rectangle2D glyphBounds = glyphVector.getVisualBounds();
double width = isSpace?spaceCharacterWidth:glyphBounds.getWidth();
double ascent = -glyphBounds.getY();
double descent = glyphBounds.getHeight()-ascent;
charactersAreSpaces[index] = isSpace;
characterGlyphVectors[index] = glyphVector;
characterGlyphBounds[index] = glyphBounds;
characterWidths[index] = width;
characterAscents[index] = ascent;
characterDescents[index] = descent;
if (squash) {
maxAscent = Math.max(maxAscent,ascent);
maxDescent = Math.max(maxDescent,descent);
}
}
double actualRadius = circularTextPosition.calculateActualRadius(radius, maxAscent, maxDescent, flip);
double currentAngleInRadians = 0;
double[] characterAnglesInRadians = new double[characterCount];
for (int index=0; index<characterCount; index++) {
characterAnglesInRadians[index] = currentAngleInRadians;
currentAngleInRadians += Math.sin(((characterWidths[index]/2.0) + leading + (characterWidths[index+1]/2.0)) / actualRadius);
}
double angleAdjustment = (centerAngleInDegrees * Math.PI / 180) - ((characterAnglesInRadians[characterCount-1] - characterAnglesInRadians[0]) / 2.0);
for (int index=0; index<characterCount; index++) {
characterAnglesInRadians[index] += angleAdjustment;
}
AffineTransform stringTransform = oldTransform;
if (stringTransform == null) {
stringTransform = new AffineTransform();
}
if (flip) {
stringTransform.rotate(Math.PI, centerX, centerY);
}
for (int index=0; index<characterCount; index++) {
if (!charactersAreSpaces[index]) {
GlyphVector glyphVector = characterGlyphVectors[index];
Rectangle2D glyphBounds = characterGlyphBounds[index];
if (flip) {
AffineTransform oldGlyphVectorTransform = glyphVector.getGlyphTransform(0);
if (oldGlyphVectorTransform == null) {
oldGlyphVectorTransform = new AffineTransform();
}
AffineTransform newGlyphVectorTransform = new AffineTransform(oldGlyphVectorTransform);
newGlyphVectorTransform.scale(-1, -1);
newGlyphVectorTransform.translate(-(glyphBounds.getWidth()+glyphBounds.getX()+glyphBounds.getX()),glyphBounds.getHeight()+(maxAscent-characterAscents[index])-characterDescents[index]-maxDescent);
glyphVector.setGlyphTransform(0, newGlyphVectorTransform);
}
AffineTransform characterTransform = new AffineTransform(stringTransform);
characterTransform.translate(centerX, centerY);
characterTransform.rotate(characterAnglesInRadians[index]);
characterTransform.translate(-((glyphBounds.getX()+(glyphBounds.getWidth()/2))),-(actualRadius+maxDescent));
g.setTransform(characterTransform);
glyphDrawer.drawGlyph(g, glyphVector.getOutline(0, 0));
}
}
glyphDrawer.restoreGraphicsStateAfterDraw(g);
g.setTransform(oldTransform);
}
}
This can then produce (Note that the text contains 'y' due to the large descent of that letter):
BASELINE_ON_CIRCLE (Not squashed):
INSIDE_CIRCLE (Not squashed):
OUTSIDE_CIRCLE (Not squashed):
I hope this code is helpful to anyone else viewing this question in the future.
My query is to show programmatically, the fitting of many given non regular (but rectangular) cubes (i.e. boxes) of individually different sizes, inside a larger volume cube, such as a storage unit.
The mathematics part is understood. Like in Linear programming / linear algebra, we can add fit volume of all smaller cubes to find out the best fit for the volume of the larger cube.
The actual requirement is to show or allow this fitting graphically on a web-page, preferably in 3d. If possible, to allow user to interact with the fitting, i.e. shuffling the placement of the cubes, etc.
Also, since I am a Java developer by profession, Java or related languages / frameworks would be my choice. However, I can use any other technology / framework / language if the end results are met.
NB: Weight is also a concern (parameter). There is a maximum weight which can be stacked in any given storage unit.
Also, since storage units can be accessed without permission (by thieves), cost of the cubes stacked in one unit is also limited. The user may desire to fit cubes of higher cost in one unit which has higher security and vice versa.
Example: allow fitting many rectangular boxes containing household electronics in a given room. The boxes maybe of TVs, refrigerators, washing machines, dishwashers, playstations, xbox 360s, etc. The different dimensions of these boxes, is to give you an idea of what to expect while fitting to the limited volume.
If there is any FOSS library / project (or even non FOSS library or project) for the same, a pointer towards it would be welcome.
Disclaimer: Okay, I know it does not 100% answer your question and also the code it veeery old (as can be concluded from the old-fashioned CVS comments) and today I would not write it that way anymore. It does still run on Java 8, though, I tested it. But in addition to solving the little informatics challenge problem of water flowing through a 3D matrix of cuboids from top to bottom depending how "leaky" the matrix (symbolising some kind of Swiss cheese) is, it also uses some very simple 3D visualisation via Java 3D. Thus, you need to install Java 3D and put the corresponding libraries onto your classpath.
The 3D output looks something like this:
package vhs.bwinfo.cheese;
// $Id: Cuboid.java,v 1.1.2.1 2006/01/10 19:48:41 Robin Exp $
import javax.media.j3d.Appearance;
import javax.media.j3d.QuadArray;
import javax.media.j3d.Shape3D;
import javax.vecmath.Point3f;
import javax.vecmath.TexCoord2f;
import javax.vecmath.Vector3f;
public class Cuboid extends Shape3D {
private static final float POS = +0.5f;
private static final float NEG = -0.5f;
private static final Point3f[] POINTS = new Point3f[] {
new Point3f(NEG, NEG, NEG),
new Point3f(POS, NEG, NEG),
new Point3f(POS, NEG, POS),
new Point3f(NEG, NEG, POS),
new Point3f(NEG, POS, NEG),
new Point3f(POS, POS, NEG),
new Point3f(POS, POS, POS),
new Point3f(NEG, POS, POS)
};
private static final TexCoord2f[] TEX_COORDS = new TexCoord2f[] {
new TexCoord2f(0, 1),
new TexCoord2f(1, 1),
new TexCoord2f(1, 0),
new TexCoord2f(0, 0)
};
private static final int VERTEX_FORMAT =
QuadArray.COORDINATES |
QuadArray.NORMALS |
QuadArray.TEXTURE_COORDINATE_2;
public Cuboid(float scaleX, float scaleY, float scaleZ) {
Point3f[] points = new Point3f[8];
for (int i = 0; i < 8; i++)
points[i] = new Point3f(
POINTS[i].x * scaleX,
POINTS[i].y * scaleY,
POINTS[i].z * scaleZ
);
Point3f[] vertices = {
points[3], points[2], points[1], points[0], // bottom
points[4], points[5], points[6], points[7], // top
points[7], points[3], points[0], points[4], // left
points[6], points[5], points[1], points[2], // right
points[7], points[6], points[2], points[3], // front
points[5], points[4], points[0], points[1] // back
};
QuadArray geometry = new QuadArray(24, VERTEX_FORMAT);
geometry.setCoordinates(0, vertices);
for (int i = 0; i < 24; i++)
geometry.setTextureCoordinate(0, i, TEX_COORDS[i % 4]);
Vector3f normal = new Vector3f();
Vector3f v1 = new Vector3f();
Vector3f v2 = new Vector3f();
Point3f[] pts = new Point3f[4];
for (int i = 0; i < 4; i++)
pts[i] = new Point3f();
for (int face = 0; face < 6; face++) {
geometry.getCoordinates(face * 4, pts);
v1.sub(pts[0], pts[2]);
v2.sub(pts[1], pts[3]);
normal.cross(v1, v2);
normal.normalize();
for (int i = 0; i < 4; i++)
geometry.setNormal((face * 4 + i), normal);
}
setGeometry(geometry);
setAppearance(new Appearance());
}
public Cuboid(float scaleFactor) {
this(scaleFactor, scaleFactor, scaleFactor);
}
}
package vhs.bwinfo.cheese;
// $Id: LeakyCheese.java,v 1.2.2.2 2006/01/10 15:37:14 Robin Exp $
import com.sun.j3d.utils.applet.JMainFrame;
import javax.swing.*;
import java.util.Random;
import static java.lang.System.out;
public class LeakyCheese {
private int width = 20, height = 20, depth = 20;
private int numClasses = 100, samplesPerClass = 100;
private double pMin = 0, pMax = 1;
private double pDiff = pMax - pMin;
private double classSize = pDiff / numClasses;
private int[] stats;
enum CubeState {CHEESE, AIR, WATER}
final private CubeState[][][] cheese;
private static final Random RND = new Random();
public LeakyCheese(
int width, int height, int depth,
int numClasses, int samplesPerClass,
double pMin, double pMax
) {
this.width = width;
this.height = height;
this.depth = depth;
this.numClasses = numClasses;
this.samplesPerClass = samplesPerClass;
this.pMin = pMin;
this.pMax = pMax;
pDiff = pMax - pMin;
classSize = pDiff / numClasses;
cheese = new CubeState[width][height][depth];
}
public LeakyCheese(
int width, int height, int depth,
int numClasses, int samplesPerClass
) {
this(width, height, depth, numClasses, samplesPerClass, 0, 1);
}
public LeakyCheese() {
cheese = new CubeState[width][height][depth];
}
private boolean pourWater(int x, int y, int z) {
if (x < 0 || x >= width || y < 0 || y >= height || z < 0 || z >= depth)
return false;
if (cheese[x][y][z] != CubeState.AIR)
return false;
cheese[x][y][z] = CubeState.WATER;
boolean retVal = (y == 0);
retVal = pourWater(x + 1, y, z) || retVal;
retVal = pourWater(x - 1, y, z) || retVal;
retVal = pourWater(x, y + 1, z) || retVal;
retVal = pourWater(x, y - 1, z) || retVal;
retVal = pourWater(x, y, z + 1) || retVal;
retVal = pourWater(x, y, z - 1) || retVal;
return retVal;
}
private boolean isLeaky(double p) {
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
for (int z = 0; z < depth; z++)
cheese[x][y][z] = (RND.nextDouble() < p)
? CubeState.CHEESE
: CubeState.AIR;
boolean retVal = false;
for (int x = 0; x < width; x++)
for (int z = 0; z < depth; z++)
retVal = pourWater(x, height - 1, z) || retVal;
return retVal;
}
private void generateStats() {
if (stats != null)
return;
stats = new int[numClasses];
for (int i = 0; i < numClasses; i++) {
for (int j = 0; j < samplesPerClass; j++) {
double p = pMin + classSize * (RND.nextDouble() + i);
if (isLeaky(p))
stats[i]++;
}
}
}
public void printStats() {
generateStats();
out.println(
"p (cheese) | p (leaky)\n" +
"------------------+-----------"
);
for (int i = 0; i < numClasses; i++) {
out.println(
String.format(
"%1.5f..%1.5f | %1.5f",
pMin + classSize * i,
pMin + classSize * (i + 1),
(double) stats[i] / samplesPerClass
)
);
}
}
public static void main(String[] args) {
//new LeakyCheese().printStats();
//new LeakyCheese(40, 40, 40, 50, 100, 0.66, .71).printStats();
LeakyCheese cheeseBlock = new LeakyCheese(5, 20, 5, 20, 100);
//LeakyCheese cheeseBlock = new LeakyCheese(20, 20, 20, 20, 100);
while (!cheeseBlock.isLeaky(0.65))
;
out.println("*** required solution found - now rendering... ***");
JMainFrame f = new JMainFrame(new LeakyCheeseGUI(cheeseBlock.cheese), 512, 512);
f.setLocationRelativeTo(null);
f.setExtendedState(JFrame.MAXIMIZED_BOTH);
}
}
package vhs.bwinfo.cheese;
// $Id: LeakyCheeseGUI.java,v 1.1.2.1 2006/01/10 15:25:18 Robin Exp $
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.universe.SimpleUniverse;
import vhs.bwinfo.cheese.LeakyCheese.CubeState;
import javax.media.j3d.*;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3f;
import java.applet.Applet;
import java.awt.*;
import java.util.Random;
public class LeakyCheeseGUI extends Applet {
static final long serialVersionUID = -8194627556699837928L;
public BranchGroup createSceneGraph(CubeState[][][] cheese) {
// Create the root of the branch graph
BranchGroup bgRoot = new BranchGroup();
// Composite of two rotations around different axes. The resulting
// TransformGroup is the parent of all our cheese cubes, because their
// orientation is identical. They only differ in their translation
// values and colours.
Transform3D tRotate = new Transform3D();
Transform3D tRotateTemp = new Transform3D();
tRotate.rotX(Math.PI / 8.0d);
tRotateTemp.rotY(Math.PI / -4.0d);
tRotate.mul(tRotateTemp);
TransformGroup tgRotate = new TransformGroup(tRotate);
bgRoot.addChild(tgRotate);
// Bounding sphere for rendering
BoundingSphere bounds =
new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 100.0);
// Set background colour
// Note: Using Canvas3D.setBackground does not work, because it is an
// AWT method. Java 3D, though, gets its background colour from its
// background node (black, if not present).
Background background = new Background(0.5f, 0.5f, 0.5f);
background.setApplicationBounds(bounds);
bgRoot.addChild(background);
TransparencyAttributes transpAttr;
// Little cheese cubes
Appearance cheeseAppearance = new Appearance();
transpAttr =
new TransparencyAttributes(TransparencyAttributes.NICEST, 0.98f);
cheeseAppearance.setTransparencyAttributes(transpAttr);
cheeseAppearance.setColoringAttributes(
new ColoringAttributes(1, 1, 0, ColoringAttributes.NICEST));
PolygonAttributes pa = new PolygonAttributes();
//pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
pa.setCullFace(PolygonAttributes.CULL_NONE);
cheeseAppearance.setPolygonAttributes(pa);
// Little water cubes
Appearance waterAppearance = new Appearance();
transpAttr =
new TransparencyAttributes(TransparencyAttributes.NICEST, 0.85f);
waterAppearance.setTransparencyAttributes(transpAttr);
waterAppearance.setColoringAttributes(
new ColoringAttributes(0, 0, 1, ColoringAttributes.NICEST));
pa = new PolygonAttributes();
pa.setCullFace(PolygonAttributes.CULL_NONE);
waterAppearance.setPolygonAttributes(pa);
// Little air cubes
Appearance airAppearance = new Appearance();
transpAttr =
new TransparencyAttributes(TransparencyAttributes.NICEST, 0.95f);
airAppearance.setTransparencyAttributes(transpAttr);
airAppearance.setColoringAttributes(
new ColoringAttributes(1, 1, 1, ColoringAttributes.NICEST));
pa = new PolygonAttributes();
//pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
pa.setCullFace(PolygonAttributes.CULL_NONE);
airAppearance.setPolygonAttributes(pa);
// Water-coloured (i.e. blue) wire frame around cheese block, if leaky
Appearance waterWireFrameAppearance = new Appearance();
waterWireFrameAppearance.setColoringAttributes(
new ColoringAttributes(0, 0, 1, ColoringAttributes.NICEST));
pa = new PolygonAttributes();
pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
pa.setCullFace(PolygonAttributes.CULL_NONE);
waterWireFrameAppearance.setPolygonAttributes(pa);
// Cheese-coloured (i.e. yellow) wire frame around cheese block, if not leaky
Appearance cheeseWireFrameAppearance = new Appearance();
cheeseWireFrameAppearance.setColoringAttributes(
new ColoringAttributes(1, 1, 0, ColoringAttributes.NICEST));
pa = new PolygonAttributes();
pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
pa.setCullFace(PolygonAttributes.CULL_NONE);
cheeseWireFrameAppearance.setPolygonAttributes(pa);
// Absolute offsets for the cheese block to fit into the viewing canvas
final float xOffs = -0.8f;
final float yOffs = -0.55f;
final float zOffs = 0;
// Create all those little cubes ;-)
final int xSize = cheese.length;
final int ySize = cheese[0].length;
final int zSize = cheese[0][0].length;
final int maxSize = Math.max(xSize, Math.max(ySize, zSize));
final float xCenterOffs = 0.5f * (maxSize - xSize) / maxSize;
final float yCenterOffs = 0.5f * (maxSize - ySize) / maxSize;
final float zCenterOffs = -0.5f * (maxSize - zSize) / maxSize;
boolean isLeaky = false;
for (int x = 0; x < xSize; x++)
for (int y = 0; y < ySize; y++)
for (int z = 0; z < zSize; z++) {
Transform3D tTranslate = new Transform3D();
tTranslate.setTranslation(
new Vector3f(
xOffs + xCenterOffs + 1.0f * x / maxSize,
yOffs + yCenterOffs + 1.0f * y / maxSize,
zOffs + zCenterOffs - 1.0f * z / maxSize
)
);
TransformGroup tgTranslate = new TransformGroup(tTranslate);
tgRotate.addChild(tgTranslate);
Cuboid cube = new Cuboid(1.0f / maxSize);
switch (cheese[x][y][z]) {
case CHEESE:
cube.setAppearance(cheeseAppearance);
break;
case WATER:
cube.setAppearance(waterAppearance);
if (y == 0)
isLeaky = true;
break;
case AIR:
cube.setAppearance(airAppearance);
}
tgTranslate.addChild(cube);
}
// If cheese block is leaky, visualise it by drawing a water-coloured
// (i.e. blue) wire frame around it. Otherwise use a cheese-coloured
// (i.e. yellow) one.
Transform3D tTranslate = new Transform3D();
tTranslate.setTranslation(
new Vector3f(
xOffs + xCenterOffs + 0.5f * (xSize - 1) / maxSize,
yOffs + yCenterOffs + 0.5f * (ySize - 1) / maxSize,
zOffs + zCenterOffs - 0.5f * (zSize - 1) / maxSize
)
);
TransformGroup tgTranslate = new TransformGroup(tTranslate);
tgRotate.addChild(tgTranslate);
Cuboid cuboid = new Cuboid(
1.0f * xSize / maxSize,
1.0f * ySize / maxSize,
1.0f * zSize / maxSize
);
cuboid.setAppearance(isLeaky ? waterWireFrameAppearance : cheeseWireFrameAppearance);
tgTranslate.addChild(cuboid);
// Let Java 3D perform optimizations on this scene graph.
bgRoot.compile();
return bgRoot;
}
public LeakyCheeseGUI(CubeState[][][] cheese) {
// Create a simple scene and attach it to the virtual universe
GraphicsConfiguration graphCfg = SimpleUniverse.getPreferredConfiguration();
Canvas3D canvas = new Canvas3D(graphCfg);
setLayout(new BorderLayout());
add(canvas, "Center");
SimpleUniverse universe = new SimpleUniverse(canvas);
// This will move the ViewPlatform back a bit so the objects
// in the scene can be viewed.
universe.getViewingPlatform().setNominalViewingTransform();
universe.addBranchGraph(createSceneGraph(cheese));
}
public static void main(String[] args) {
final Random RND = new Random(System.currentTimeMillis());
CubeState[][][] testCheese = new CubeState[5][8][11];
for (int x = 0; x < 5; x++)
for (int y = 0; y < 8; y++)
for (int z = 0; z < 11; z++)
testCheese[x][y][z] = (RND.nextFloat() < 0.7f)
? CubeState.CHEESE
: (RND.nextBoolean() ? CubeState.WATER : CubeState.AIR);
// Applet can also run as a stand-alone application
new MainFrame(new LeakyCheeseGUI(testCheese), 512, 512);
}
}
You will probably want to use Javascript, and specifically WebGL. Javascript is the de facto language for interactive web pages, and WebGL is a Javascript API for rendering 2D and 3D scenes on an HTML5 canvas element. A solution using WebGL should be compatible with all major browsers. Programming even simple scenes in WebGL can be pretty involved though, so I'd recommend using a framework such as three.js to simplify things.
Here is an example of interactive, draggable cubes using three.js. Some of the key lines of code from that example are:
// create the cube
var object = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: Math.random() * 0xffffff } ) );
// set coordinates, rotation, and scale of the cubes
object.position.x = ...
object.position.y = ...
object.position.z = ...
object.rotation.x = ...
object.rotation.y = ...
object.rotation.z = ...
object.scale.x = ...
object.scale.y = ...
object.scale.z = ...
// lighting stuff
object.castShadow = true;
object.receiveShadow = true;
// add to scene and list of objects
scene.add( object );
objects.push( object );
Again, the full, working example is found at this link (click view source on that page to view the code on github).
I am writing a darts application, and have implemented a Dartboard which is painted as a BufferedImage.
When rendering the dartboard, I first iterate over the co-ordinates of the BufferedImage and calculate the 'segment' that it resides in. I wrap this up into a DartboardSegment, which is basically just a collection of points with a small amount of extra structure (what number on the board it corresponds to, etc).
Currently, to actually render the dartboard I paint each point individually, like the following:
for (Point pt : allPoints)
{
DartboardSegment segment = getSegmentForPoint(pt);
Color colour = DartboardUtil.getColourForSegment(segment);
int rgb = colour.getRGB();
int x = (int)pt.getX();
int y = (int)pt.getY();
dartboardImage.setRGB(x, y, rgb);
}
Obviously this takes some time. It's not an intolerable amount (~2-3s to paint a 500x500 area), but I'd like to eliminate this 'lag' if I can. In other areas of my application I have encountered alternate methods (such as Graphics.fillRect()) which are much faster.
I've seen that there is a fillPolgyon() method on the Graphics class, however I don't think I can easily convert my segments into polygons because their shapes vary (e.g. the shape of a triple, a circle for the bullseye...). Is there a faster way in java to paint an arbitrary array of Points at once, rather than looping through and painting individually?
The code that I want to write is something like:
for (DartboardSegment segment : allSegments)
{
Color colour = DartboardUtil.getColourForSegment(segment);
Polgyon poly = segment.toPolygon();
Graphics gfx = dartboardImage.getGraphics();
gfx.setColor(colour);
gfx.fillPolygon(poly);
}
I don't think I can easily convert my segments into polygons because their shapes vary (e.g. the shape of a triple, a circle for the bullseye...)
Here is something that may give you some ideas.
You can create Shape objects to represent each area of the dartboard:
import java.awt.*;
import java.util.*;
import javax.swing.*;
import java.awt.geom.*;
public class Dartboard extends JPanel
{
private ArrayList<DartboardSegment> segments = new ArrayList<DartboardSegment>();
private int size = 500;
private int radius = size / 2;
private int border = 25;
private int doubleSize = size - (2 * border);
private int tripleSize = size / 2;
private int thickness = 10;
public Dartboard()
{
createSegmentWedges();
int innerRadius = size - (2 * border);
Shape outer = new Ellipse2D.Double(0, 0, size, size);
Shape inner = new Ellipse2D.Double(border, border, innerRadius, innerRadius);
Area circle = new Area( outer );
circle.subtract( new Area(inner) );
segments.add( new DartboardSegment(circle, Color.BLACK) );
createBullsEye();
}
private void createSegmentWedges()
{
int angle = -99;
for (int i = 0; i < 20; i++)
{
// Create the wedge shape
GeneralPath path = new GeneralPath();
path.moveTo(250, 250);
double radians1 = Math.toRadians( angle );
double x1 = Math.cos(radians1) * radius;
double y1 = Math.sin(radians1) * radius;
path.lineTo(x1 + 250, y1 + 250);
angle += 18;
double radians2 = Math.toRadians( angle );
double x2 = Math.cos(radians2) * radius;
double y2 = Math.sin(radians2) * radius;
path.lineTo(x2 + 250, y2 + 250);
path.closePath();
Color wedgeColor = (i % 2 == 0) ? Color.BLACK : Color.WHITE;
segments.add( new DartboardSegment(path, wedgeColor) );
// Create the double/triple shapes
Color color = (i % 2 == 0) ? Color.RED : Color.GREEN;
createShape(doubleSize, path, color);
createShape(tripleSize, path, color);
}
}
private void createShape(int outerSize, GeneralPath path, Color color)
{
int outerOffset = (size - outerSize) / 2;
int innerSize = outerSize - (2 * thickness);
int innerOffset = (size - innerSize) / 2;
Shape outer = new Ellipse2D.Double(outerOffset, outerOffset, outerSize, outerSize);
Shape inner = new Ellipse2D.Double(innerOffset, innerOffset, innerSize, innerSize);
Area circle = new Area( outer );
circle.subtract( new Area(inner) );
circle.intersect( new Area(path) );
segments.add( new DartboardSegment(circle, color) );
}
private void createBullsEye()
{
int radius1 = 40;
int offset1 = (size - radius1) / 2;
Ellipse2D.Double bullsEye1 = new Ellipse2D.Double(offset1, offset1, radius1, radius1);
segments.add( new DartboardSegment(bullsEye1, Color.GREEN) );
int radius2 = 20;
int offset2 = (size - radius2) / 2;
Ellipse2D.Double bullsEye2 = new Ellipse2D.Double(offset2, offset2, radius2, radius2);
segments.add( new DartboardSegment(bullsEye2, Color.RED) );
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2d = (Graphics2D)g.create();
for (DartboardSegment segment: segments)
{
g2d.setColor( segment.getColor() );
g2d.fill( segment.getShape() );
}
}
#Override
public Dimension getPreferredSize()
{
return new Dimension(500, 500);
}
class DartboardSegment
{
private Shape shape;
private Color color;
public DartboardSegment(Shape shape, Color color)
{
this.shape = shape;
this.color = color;
}
public Shape getShape()
{
return shape;
}
public Color getColor()
{
return color;
}
}
private static void createAndShowGUI()
{
JFrame frame = new JFrame("DartBoard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new Dartboard());
frame.setLocationByPlatform( true );
frame.pack();
frame.setVisible( true );
}
public static void main(String[] args)
{
EventQueue.invokeLater( () -> createAndShowGUI() );
/*
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
*/
}
}
After a bit more digging, I think one solution to this is to do the following. It's not the neatest, but I think it will work:
int i = 0;
for (int y=0; y<height; y++)
{
for (int x=0; x<width; x++)
{
Point pt = new Point(x, y);
DartboardSegment segment = getSegmentForPoint(pt);
Color colour = DartboardUtil.getColourForSegment(segment);
pixels[i] = colorToUse.getRGB();
i++;
}
}
dartboardImage.setRGB(0, 0, width, height, pixels, 0, width);
I am open to better suggestions, however!
I'm new to Javafx and I'm experimenting with animations. Following this, I've created a curve with two anchor points. Moving the anchor points changes the shape of the curve. Next, I followed this to create an animation where a square follows the curve from one end point to the other.
Combining those two works fine, except when I move one of the anchor points! My square keeps following the original trajectory. Any suggestions on how to fix this? I don't want to restart the animation; the square should just continue moving along its path without visible interruption.
Here's a complete working example:
import javafx.animation.PathTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CurveAnimation extends Application {
public static void main(String[] args) throws Exception { launch(args); }
#Override
public void start(final Stage stage) throws Exception {
//Create a curve
CubicCurve curve = new CubicCurve();
curve.setStartX(100);
curve.setStartY(100);
curve.setControlX1(150);
curve.setControlY1(50);
curve.setControlX2(250);
curve.setControlY2(150);
curve.setEndX(300);
curve.setEndY(100);
curve.setStroke(Color.FORESTGREEN);
curve.setStrokeWidth(4);
curve.setFill(Color.CORNSILK.deriveColor(0, 1.2, 1, 0.6));
//Create anchor points at each end of the curve
Anchor start = new Anchor(Color.PALEGREEN, curve.startXProperty(), curve.startYProperty());
Anchor end = new Anchor(Color.TOMATO, curve.endXProperty(), curve.endYProperty());
//Create object that follows the curve
Rectangle rectPath = new Rectangle (0, 0, 40, 40);
rectPath.setArcHeight(25);
rectPath.setArcWidth(25);
rectPath.setFill(Color.ORANGE);
//Create the animation
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(2000));
pathTransition.setPath(curve);
pathTransition.setNode(rectPath);
pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
pathTransition.setCycleCount(Timeline.INDEFINITE);
pathTransition.setAutoReverse(true);
pathTransition.play();
Group root = new Group();
root.getChildren().addAll(curve, start, end, rectPath);
stage.setScene(new Scene( root, 400, 400, Color.ALICEBLUE));
stage.show();
}
/**
* Create draggable anchor points
*/
class Anchor extends Circle {
Anchor(Color color, DoubleProperty x, DoubleProperty y) {
super(x.get(), y.get(), 10);
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
x.bind(centerXProperty());
y.bind(centerYProperty());
enableDrag();
}
// make a node movable by dragging it around with the mouse.
private void enableDrag() {
final Delta dragDelta = new Delta();
setOnMousePressed(mouseEvent -> {
// record a delta distance for the drag and drop operation.
dragDelta.x = getCenterX() - mouseEvent.getX();
dragDelta.y = getCenterY() - mouseEvent.getY();
getScene().setCursor(Cursor.MOVE);
});
setOnMouseReleased(mouseEvent -> getScene().setCursor(Cursor.HAND));
setOnMouseDragged(mouseEvent -> {
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
setCenterX(newX);
}
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
setCenterY(newY);
}
});
setOnMouseEntered(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.HAND);
}
});
setOnMouseExited(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
});
}
// records relative x and y co-ordinates.
private class Delta { double x, y; }
}
}
The PathTransition apparently just copies the values from the path when you call setPath, and doesn't observe them if they change.
To do what you want, you will need to use a Transition and implement the interpolation yourself. The interpolation must take a value double t and set the translateX and translateY properties of the node so that its center is on the curve with parameter t. If you want the ORTHOGONAL_TO_TANGENT orientation, you will also need to set the rotate property of the node to the angle of the tangent of the cubic curve to the positive horizontal. By computing these in the interpolate method, you can simply refer to the current control points of the curve.
To do the computation, you need to know a bit of geometry. The point on a linear Bezier curve with control points (i.e. start and end) P0 and P1 at parameter t is given by
B(t; P0, P1) = (1-t)*P0 + t*P1
You can compute higher order Bezier curves recursively by
B(t; P0, P1, ..., Pn) = (1-t)*B(P0, P1, ..., P(n-1); t) + t*B(P1, P2, ..., Pn;t)
and just differentiate both of those to get the tangent for the linear curve (which is, from geometrical consideration, obviously just P1-P0) and for the recursive relationship:
B'(t; P0, P1) = -P0 + P1
and
B'(t; P0, P1, ..., Pn) = -B(t; P0, ..., P(n-1)) + (1-t)B'(t; P0, ..., P(n-1))
+ B(t; P1, ..., Pn) + tB'(t; P1, ..., Pn)
Here is this implemented in code:
import javafx.animation.Animation;
import javafx.animation.PathTransition;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CurveAnimation extends Application {
public static void main(String[] args) throws Exception { launch(args); }
#Override
public void start(final Stage stage) throws Exception {
//Create a curve
CubicCurve curve = new CubicCurve();
curve.setStartX(100);
curve.setStartY(100);
curve.setControlX1(150);
curve.setControlY1(50);
curve.setControlX2(250);
curve.setControlY2(150);
curve.setEndX(300);
curve.setEndY(100);
curve.setStroke(Color.FORESTGREEN);
curve.setStrokeWidth(4);
curve.setFill(Color.CORNSILK.deriveColor(0, 1.2, 1, 0.6));
//Create anchor points at each end of the curve
Anchor start = new Anchor(Color.PALEGREEN, curve.startXProperty(), curve.startYProperty());
Anchor end = new Anchor(Color.TOMATO, curve.endXProperty(), curve.endYProperty());
//Create object that follows the curve
Rectangle rectPath = new Rectangle (0, 0, 40, 40);
rectPath.setArcHeight(25);
rectPath.setArcWidth(25);
rectPath.setFill(Color.ORANGE);
Transition transition = new Transition() {
{
setCycleDuration(Duration.millis(2000));
}
#Override
protected void interpolate(double frac) {
Point2D start = new Point2D(curve.getStartX(), curve.getStartY());
Point2D control1 = new Point2D(curve.getControlX1(), curve.getControlY1());
Point2D control2 = new Point2D(curve.getControlX2(), curve.getControlY2());
Point2D end = new Point2D(curve.getEndX(), curve.getEndY());
Point2D center = bezier(frac, start, control1, control2, end);
double width = rectPath.getBoundsInLocal().getWidth() ;
double height = rectPath.getBoundsInLocal().getHeight() ;
rectPath.setTranslateX(center.getX() - width /2);
rectPath.setTranslateY(center.getY() - height / 2);
Point2D tangent = bezierDeriv(frac, start, control1, control2, end);
double angle = Math.toDegrees(Math.atan2(tangent.getY(), tangent.getX()));
rectPath.setRotate(angle);
}
};
transition.setCycleCount(Animation.INDEFINITE);
transition.setAutoReverse(true);
transition.play();
Group root = new Group();
root.getChildren().addAll(curve, start, end, rectPath);
stage.setScene(new Scene( root, 400, 400, Color.ALICEBLUE));
stage.show();
}
private Point2D bezier(double t, Point2D... points) {
if (points.length == 2) {
return points[0].multiply(1-t).add(points[1].multiply(t));
}
Point2D[] leftArray = new Point2D[points.length - 1];
System.arraycopy(points, 0, leftArray, 0, points.length - 1);
Point2D[] rightArray = new Point2D[points.length - 1];
System.arraycopy(points, 1, rightArray, 0, points.length - 1);
return bezier(t, leftArray).multiply(1-t).add(bezier(t, rightArray).multiply(t));
}
private Point2D bezierDeriv(double t, Point2D... points) {
if (points.length == 2) {
return points[1].subtract(points[0]);
}
Point2D[] leftArray = new Point2D[points.length - 1];
System.arraycopy(points, 0, leftArray, 0, points.length - 1);
Point2D[] rightArray = new Point2D[points.length - 1];
System.arraycopy(points, 1, rightArray, 0, points.length - 1);
return bezier(t, leftArray).multiply(-1).add(bezierDeriv(t, leftArray).multiply(1-t))
.add(bezier(t, rightArray)).add(bezierDeriv(t, rightArray).multiply(t));
}
/**
* Create draggable anchor points
*/
class Anchor extends Circle {
Anchor(Color color, DoubleProperty x, DoubleProperty y) {
super(x.get(), y.get(), 10);
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
x.bind(centerXProperty());
y.bind(centerYProperty());
enableDrag();
}
// make a node movable by dragging it around with the mouse.
private void enableDrag() {
final Delta dragDelta = new Delta();
setOnMousePressed(mouseEvent -> {
// record a delta distance for the drag and drop operation.
dragDelta.x = getCenterX() - mouseEvent.getX();
dragDelta.y = getCenterY() - mouseEvent.getY();
getScene().setCursor(Cursor.MOVE);
});
setOnMouseReleased(mouseEvent -> getScene().setCursor(Cursor.HAND));
setOnMouseDragged(mouseEvent -> {
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
setCenterX(newX);
}
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
setCenterY(newY);
}
});
setOnMouseEntered(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.HAND);
}
});
setOnMouseExited(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
});
}
// records relative x and y co-ordinates.
private class Delta { double x, y; }
}
}
I don't know if it's the code, the math, or just the animation, but this is somehow deeply satisfying...
I've found this example of how to render line of sight in WorldWind: http://patmurris.blogspot.com/2008/04/ray-casting-and-line-of-sight-for-wwj.html (its a bit old, but it still seems to work). This is the class used in the example (slightly modified code below to work with WorldWind 2.0). It looks like the code also uses RayCastingSupport (Javadoc and Code) to do its magic.
What I'm trying to figure out is if this code/example is using the curvature of the earth/and or the distance to the horizon as part of its logic. Just looking at the code, I'm not sure I understand completely what it is doing.
For instance, if I was trying to figure out what terrain a person 200 meters above the earth could "see", would it take the distance to the horizon into account?
What would it take to modify the code to account for distance to the horizon/curvature of the earth (if its not already)?
package gov.nasa.worldwindx.examples;
import gov.nasa.worldwind.util.RayCastingSupport;
import gov.nasa.worldwind.view.orbit.OrbitView;
import gov.nasa.worldwind.geom.Angle;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.geom.Sector;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.layers.CrosshairLayer;
import gov.nasa.worldwind.layers.RenderableLayer;
import gov.nasa.worldwind.render.*;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
public class LineOfSight extends ApplicationTemplate
{
public static class AppFrame extends ApplicationTemplate.AppFrame
{
private double samplingLength = 30; // Ray casting sample length
private int centerOffset = 100; // meters above ground for center
private int pointOffset = 10; // meters above ground for sampled points
private Vec4 light = new Vec4(1, 1, -1).normalize3(); // Light direction (from South-East)
private double ambiant = .4; // Minimum lighting (0 - 1)
private RenderableLayer renderableLayer;
private SurfaceImage surfaceImage;
private ScreenAnnotation screenAnnotation;
private JComboBox radiusCombo;
private JComboBox samplesCombo;
private JCheckBox shadingCheck;
private JButton computeButton;
public AppFrame()
{
super(true, true, false);
// Add USGS Topo Maps
// insertBeforePlacenames(getWwd(), new USGSTopographicMaps());
// Add our renderable layer for result display
this.renderableLayer = new RenderableLayer();
this.renderableLayer.setName("Line of sight");
this.renderableLayer.setPickEnabled(false);
insertBeforePlacenames(getWwd(), this.renderableLayer);
// Add crosshair layer
insertBeforePlacenames(getWwd(), new CrosshairLayer());
// Update layer panel
this.getLayerPanel().update(getWwd());
// Add control panel
this.getLayerPanel().add(makeControlPanel(), BorderLayout.SOUTH);
}
private JPanel makeControlPanel()
{
JPanel controlPanel = new JPanel(new GridLayout(0, 1, 0, 0));
controlPanel.setBorder(
new CompoundBorder(BorderFactory.createEmptyBorder(9, 9, 9, 9),
new TitledBorder("Line Of Sight")));
// Radius combo
JPanel radiusPanel = new JPanel(new GridLayout(0, 2, 0, 0));
radiusPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
radiusPanel.add(new JLabel("Max radius:"));
radiusCombo = new JComboBox(new String[] {"5km", "10km",
"20km", "30km", "50km", "100km", "200km"});
radiusCombo.setSelectedItem("10km");
radiusPanel.add(radiusCombo);
// Samples combo
JPanel samplesPanel = new JPanel(new GridLayout(0, 2, 0, 0));
samplesPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
samplesPanel.add(new JLabel("Samples:"));
samplesCombo = new JComboBox(new String[] {"128", "256", "512"});
samplesCombo.setSelectedItem("128");
samplesPanel.add(samplesCombo);
// Shading checkbox
JPanel shadingPanel = new JPanel(new GridLayout(0, 2, 0, 0));
shadingPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
shadingPanel.add(new JLabel("Light:"));
shadingCheck = new JCheckBox("Add shading");
shadingCheck.setSelected(false);
shadingPanel.add(shadingCheck);
// Compute button
JPanel buttonPanel = new JPanel(new GridLayout(0, 1, 0, 0));
buttonPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
computeButton = new JButton("Compute");
computeButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent actionEvent)
{
update();
}
});
buttonPanel.add(computeButton);
// Help text
JPanel helpPanel = new JPanel(new GridLayout(0, 1, 0, 0));
buttonPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
helpPanel.add(new JLabel("Place view center on an elevated"));
helpPanel.add(new JLabel("location and click \"Compute\""));
// Panel assembly
controlPanel.add(radiusPanel);
controlPanel.add(samplesPanel);
controlPanel.add(shadingPanel);
controlPanel.add(buttonPanel);
controlPanel.add(helpPanel);
return controlPanel;
}
// Update line of sight computation
private void update()
{
new Thread(new Runnable() {
public void run()
{
computeLineOfSight();
}
}, "LOS thread").start();
}
private void computeLineOfSight()
{
computeButton.setEnabled(false);
computeButton.setText("Computing...");
try
{
Globe globe = getWwd().getModel().getGlobe();
OrbitView view = (OrbitView)getWwd().getView();
Position centerPosition = view.getCenterPosition();
// Compute sector
String radiusString = ((String)radiusCombo.getSelectedItem());
double radius = 1000 * Double.parseDouble(radiusString.substring(0, radiusString.length() - 2));
double deltaLatRadians = radius / globe.getEquatorialRadius();
double deltaLonRadians = deltaLatRadians / Math.cos(centerPosition.getLatitude().radians);
Sector sector = new Sector(centerPosition.getLatitude().subtractRadians(deltaLatRadians),
centerPosition.getLatitude().addRadians(deltaLatRadians),
centerPosition.getLongitude().subtractRadians(deltaLonRadians),
centerPosition.getLongitude().addRadians(deltaLonRadians));
// Compute center point
double centerElevation = globe.getElevation(centerPosition.getLatitude(),
centerPosition.getLongitude());
Vec4 center = globe.computePointFromPosition(
new Position(centerPosition, centerElevation + centerOffset));
// Compute image
float hueScaleFactor = .7f;
int samples = Integer.parseInt((String)samplesCombo.getSelectedItem());
BufferedImage image = new BufferedImage(samples, samples, BufferedImage.TYPE_4BYTE_ABGR);
double latStepRadians = sector.getDeltaLatRadians() / image.getHeight();
double lonStepRadians = sector.getDeltaLonRadians() / image.getWidth();
for (int x = 0; x < image.getWidth(); x++)
{
Angle lon = sector.getMinLongitude().addRadians(lonStepRadians * x + lonStepRadians / 2);
for (int y = 0; y < image.getHeight(); y++)
{
Angle lat = sector.getMaxLatitude().subtractRadians(latStepRadians * y + latStepRadians / 2);
double el = globe.getElevation(lat, lon);
// Test line of sight from point to center
Vec4 point = globe.computePointFromPosition(lat, lon, el + pointOffset);
double distance = point.distanceTo3(center);
if (distance <= radius)
{
if (RayCastingSupport.intersectSegmentWithTerrain(
globe, point, center, samplingLength, samplingLength) == null)
{
// Center visible from point: set pixel color and shade
float hue = (float)Math.min(distance / radius, 1) * hueScaleFactor;
float shade = shadingCheck.isSelected() ?
(float)computeShading(globe, lat, lon, light, ambiant) : 0f;
image.setRGB(x, y, Color.HSBtoRGB(hue, 1f, 1f - shade));
}
else if (shadingCheck.isSelected())
{
// Center not visible: apply shading nonetheless if selected
float shade = (float)computeShading(globe, lat, lon, light, ambiant);
image.setRGB(x, y, new Color(0f, 0f, 0f, shade).getRGB());
}
}
}
}
// Blur image
PatternFactory.blur(PatternFactory.blur(PatternFactory.blur(PatternFactory.blur(image))));
// Update surface image
if (this.surfaceImage != null)
this.renderableLayer.removeRenderable(this.surfaceImage);
this.surfaceImage = new SurfaceImage(image, sector);
this.surfaceImage.setOpacity(.5);
this.renderableLayer.addRenderable(this.surfaceImage);
// Compute distance scale image
BufferedImage scaleImage = new BufferedImage(64, 256, BufferedImage.TYPE_4BYTE_ABGR);
Graphics g2 = scaleImage.getGraphics();
int divisions = 10;
int labelStep = scaleImage.getHeight() / divisions;
for (int y = 0; y < scaleImage.getHeight(); y++)
{
int x1 = scaleImage.getWidth() / 5;
if (y % labelStep == 0 && y != 0)
{
double d = radius / divisions * y / labelStep / 1000;
String label = Double.toString(d) + "km";
g2.setColor(Color.BLACK);
g2.drawString(label, x1 + 6, y + 6);
g2.setColor(Color.WHITE);
g2.drawLine(x1, y, x1 + 4 , y);
g2.drawString(label, x1 + 5, y + 5);
}
float hue = (float)y / (scaleImage.getHeight() - 1) * hueScaleFactor;
g2.setColor(Color.getHSBColor(hue, 1f, 1f));
g2.drawLine(0, y, x1, y);
}
// Update distance scale screen annotation
if (this.screenAnnotation != null)
this.renderableLayer.removeRenderable(this.screenAnnotation);
this.screenAnnotation = new ScreenAnnotation("", new Point(20, 20));
this.screenAnnotation.getAttributes().setImageSource(scaleImage);
this.screenAnnotation.getAttributes().setSize(
new Dimension(scaleImage.getWidth(), scaleImage.getHeight()));
this.screenAnnotation.getAttributes().setAdjustWidthToText(Annotation.SIZE_FIXED);
this.screenAnnotation.getAttributes().setDrawOffset(new Point(scaleImage.getWidth() / 2, 0));
this.screenAnnotation.getAttributes().setBorderWidth(0);
this.screenAnnotation.getAttributes().setCornerRadius(0);
this.screenAnnotation.getAttributes().setBackgroundColor(new Color(0f, 0f, 0f, 0f));
this.renderableLayer.addRenderable(this.screenAnnotation);
// Redraw
this.getWwd().redraw();
}
finally
{
computeButton.setEnabled(true);
computeButton.setText("Compute");
}
}
/**
* Compute shadow intensity at a globe position.
* #param globe the <code>Globe</code>.
* #param lat the location latitude.
* #param lon the location longitude.
* #param light the light direction vector. Expected to be normalized.
* #param ambiant the minimum ambiant light level (0..1).
* #return the shadow intensity for the location. No shadow = 0, totaly obscured = 1.
*/
private static double computeShading(Globe globe, Angle lat, Angle lon, Vec4 light, double ambiant)
{
double thirtyMetersRadians = 30 / globe.getEquatorialRadius();
Vec4 p0 = globe.computePointFromPosition(lat, lon, 0);
Vec4 px = globe.computePointFromPosition(lat, Angle.fromRadians(lon.radians - thirtyMetersRadians), 0);
Vec4 py = globe.computePointFromPosition(Angle.fromRadians(lat.radians + thirtyMetersRadians), lon, 0);
double el0 = globe.getElevation(lat, lon);
double elx = globe.getElevation(lat, Angle.fromRadians(lon.radians - thirtyMetersRadians));
double ely = globe.getElevation(Angle.fromRadians(lat.radians + thirtyMetersRadians), lon);
Vec4 vx = new Vec4(p0.distanceTo3(px), 0, elx - el0).normalize3();
Vec4 vy = new Vec4(0, p0.distanceTo3(py), ely - el0).normalize3();
Vec4 normal = vx.cross3(vy).normalize3();
return 1d - Math.max(-light.dot3(normal), ambiant);
}
}
public static void main(String[] args)
{
ApplicationTemplate.start("World Wind Line Of Sight Calculation", AppFrame.class);
}
}
You are correct. This code does not take into account the earth curve.
From what I could see, a ray trace is done for the center of the light but the cone of the light was drawn on an image (I am not sure about that, but it looks as if this example draws on an image of gray scale).
Any way this demo is about detecting hitting the ground to stop the ray trace.
From what I understand, the algorithm stops after a distance set in the form (5km,10km ... 200km etc.)
I don't understand the direction of the ray. It makes sense to check for 200km radius only if you check light from out of space....
If you want to take the horizon into account you should check the pitch of the light source first. Its relevant for positive pitch values (above the horizon).
In that case you should decide when to stop once the center of the light gets very high above ground. How high depends on whether you point your light towards a mountain slope of you terrain is relatively flat, or if the source of light is narrow beam or wide.