I'm trying to get an array of objects to render into a gamescreen. I've looked around and found some similar questions asked by others, but I can't seem to apply the answers they've gotten to my program.
The problem seems to occur in the nested for-loops. In trying to solve this issue, I've gotten Java NullPointerExceptions at the first line within those for loops.
package com.frfanizz.agility;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL30;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
public class GameScreen implements Screen {
AgilityGame game;
OrthographicCamera camera;
SpriteBatch batch;
Hero hero;
Spark[] sparkArray;
int totalSparks;
public GameScreen(AgilityGame game) {
this.game = game;
camera = new OrthographicCamera();
camera.setToOrtho(true, 1920, 1080);
batch = new SpriteBatch();
hero = new Hero(60,540);
//Variables to set spark array
int screenX = 1400;
int screenY = 1200;
int numOfRow = 11;
int numOfCol = 8;
float spacingRow = screenY/(numOfRow + 1);
float spacingCol = screenX/(numOfCol + 1);
int totalSparks = numOfRow*numOfCol;
//Spark array
Spark[] sparkArray = new Spark[totalSparks];
//setting bounds for sparks
int index = 0;
for (int i=0;i<numOfCol;i++) {
for (int j=0; j<numOfRow;j++) {
//sparkArray[index] = new Spark();
sparkArray[index].bounds.x = (float) (60 + spacingCol + ((i)*spacingCol));
sparkArray[index].bounds.y = (float) (spacingRow + ((j-0.5)*spacingRow));
index++;
}
}
}
#Override
public void render(float delta) {
Gdx.gl.glClearColor(1F, 1F, 1F, 1F);
Gdx.gl.glClear(GL30.GL_COLOR_BUFFER_BIT);
camera.update();
generalUpdate();
batch.setProjectionMatrix(camera.combined);
batch.begin();
//Rendering code
batch.draw(Assets.spriteBackground, 0, 0);
batch.draw(hero.image,hero.bounds.x,hero.bounds.y);
for (int i=0; i<totalSparks; i++) {
batch.draw(sparkArray[i].image,sparkArray[i].bounds.x,sparkArray[i].bounds.y);
}
batch.end();
}
//Other gamescreen methods
}
The classes Hero and Spark are as follows:
package com.frfanizz.agility;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.math.Rectangle;
public class Hero {
public Sprite image;
public Rectangle bounds;
public Hero(int spawnX, int spawnY) {
image = Assets.spriteHero;
image.flip(false, true);
bounds = new Rectangle(spawnX - 16, spawnY - 16, 32, 32);
}
}
and:
package com.frfanizz.agility;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.math.Rectangle;
public class Spark {
public Sprite image;
public Rectangle bounds;
public Spark () {
image = Assets.spriteSpark;
image.flip(false, true);
bounds = new Rectangle();
bounds.height = 32;
bounds.width = 32;
}
}
Within the for loops, I've printed the values of sparkArray, so I think I have an issue with values rather than references from the other questions I've read the answers to.
(Here are the questions I (unsuccessfully) referenced:
Java. Array of objects ,
Java NullPointerException with objects in array)
Thanks in advance!
Uncomment the line
//sparkArray[index] = new Spark();
Your array of Spark objects contains null values until you put instances of Spark into it.
Consider using a two-dimensional array of Spark, if it's appropriate to what you're doing.
int numOfRow = 11;
int numOfCol = 8;
Spark[][] sparkArray = new Spark[numOfRow][numOfCol];
//setting bounds for sparks
for (int row=0; row<numOfRow; row++) {
for (int col=0; col<numOfCol; col++) {
sparkArray[row][col] = new Spark();
sparkArray[row][col].bounds.x = 1.23; // example of usage
}
}
Related
so I am importing glTF format into JavaFX. I'm experiencing a weird effect where I can see through the Triangle mesh and see other parts of it / see other meshes through it.
A few Pics
It would be fantastic if someone knows a solution to this. I already tried using CullFace.Front and CullFace.Back.
Here is the code the generate the Triangle Meshes:
package fx;
import gltf.GLTFParseException;
import gltf.mesh.GLTFMesh;
import gltf.mesh.GLTFMeshPrimitive;
import javafx.scene.Group;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import java.util.Arrays;
public class FXglTFMesh extends Group {
public static FXglTFMesh fromGLTFMesh(GLTFMesh mesh) throws GLTFParseException {
FXglTFMesh fxMesh = new FXglTFMesh();
GLTFMeshPrimitive[] primitives = mesh.getPrimitives();
MeshView[] meshViews = new MeshView[primitives.length];
for (int i = 0;i < meshViews.length; i++){
meshViews[i] = fromPrimitive(primitives[i]);
}
FXglTFMaterial material;
fxMesh.getChildren().addAll(meshViews);
return fxMesh;
}
private static MeshView fromPrimitive(GLTFMeshPrimitive primitive) throws GLTFParseException {
TriangleMesh mesh = new TriangleMesh();
MeshView view = new MeshView(mesh);
view.setCullFace(CullFace.BACK);
//Reading texture coords
float[][] texCoords = convertArrayToNested(2,
primitive.getAttribute().getTexCoord_0().readDataAsFloats());
mesh.getTexCoords().addAll(primitive.getAttribute().getTexCoord_0().readDataAsFloats());
if (primitive.getIndices() != null){
//Mesh is indexed
mesh.getPoints().addAll(primitive.getAttribute().getPosition().readDataAsFloats());
int[] indices = primitive.getIndices().readDataAsInts();
for (int i = 0;i < indices.length; i+=3){
mesh.getFaces().addAll(indices[i], 0, indices[i+1], 0, indices[i+2], 0);
}
} else {
//Mesh is not indexed
//Parse the vertices and faces
float[][] vertices = convertArrayToNested(3,
primitive.getAttribute().getPosition().readDataAsFloats());
for (int i = 0;i < vertices.length; i+=3){
mesh.getPoints().addAll(vertices[i]);
mesh.getPoints().addAll(vertices[i+1]);
mesh.getPoints().addAll(vertices[i+2]);
//add 3 points
mesh.getFaces().addAll(i,i, i+1,i+1, i+2,i+2 ); //Add those three points as face
}
}
//Material
FXglTFMaterial material = FXglTFMaterial.fromGLTFMaterial(primitive.getMaterial());
view.setMaterial(material);
return view;
}
private static float[][] convertArrayToNested(int factor, float[] array){
float[][] floats = new float[array.length / factor][];
for (int i = 0;i < floats.length; i++){
int dataOffset = i * factor;
floats[i] = Arrays.copyOfRange(array, dataOffset, dataOffset+factor);
}
return floats;
}
}
Full code: https://github.com/NekoLvds/GLTFImporter (Development branch)
My application generates heatmap images as fast as the CPU can (around 30-60 per second) and I want to display them in a single "live heatmap". In AWT/Swing, I could just paint them into a JPanel which worked like a charm.
Recently, I switched to JavaFX and want to achieve the same here; at first, I tried with a Canvas, which was slow but okay-ish but had a severe memory leak problem, causing the application to crash. Now, I tried the ImageView component - which apparently is way too slow as the image gets quite laggy (using ImageView.setImage on every new iteration). As far as I understand, setImage does not guarantee that the image is actually displayed when the function finishes.
I am getting the impression that I am on the wrong track, using those components in a manner they are not made to. How can I display my 30-60 Images per second?
EDIT: A very simple test application. You will need the JHeatChart library.
Note that on a desktop machine, I get around 70-80 FPS and the visualization is okay and fluid, but on a smaller raspberry pi (my target machine), I get around 30 FPS but an extremly stucking visualization.
package sample;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.tc33.jheatchart.HeatChart;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
public class Main extends Application {
ImageView imageView = new ImageView();
final int scale = 15;
#Override
public void start(Stage primaryStage) {
Thread generator = new Thread(() -> {
int col = 0;
LinkedList<Long> fps = new LinkedList<>();
while (true) {
fps.add(System.currentTimeMillis());
double[][] matrix = new double[48][128];
for (int i = 0; i < 48; i++) {
for (int j = 0; j < 128; j++) {
matrix[i][j] = col == j ? Math.random() : 0;
}
}
col = (col + 1) % 128;
HeatChart heatChart = new HeatChart(matrix, 0, 1);
heatChart.setShowXAxisValues(false);
heatChart.setShowYAxisValues(false);
heatChart.setLowValueColour(java.awt.Color.black);
heatChart.setHighValueColour(java.awt.Color.white);
heatChart.setAxisThickness(0);
heatChart.setChartMargin(0);
heatChart.setCellSize(new Dimension(1, 1));
long currentTime = System.currentTimeMillis();
fps.removeIf(elem -> currentTime - elem > 1000);
System.out.println(fps.size());
imageView.setImage(SwingFXUtils.toFXImage((BufferedImage) scale(heatChart.getChartImage(), scale), null));
}
});
VBox box = new VBox();
box.getChildren().add(imageView);
Scene scene = new Scene(box, 1920, 720);
primaryStage.setScene(scene);
primaryStage.show();
generator.start();
}
public static void main(String[] args) {
launch(args);
}
private static Image scale(Image image, int scale) {
BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(scale, scale);
AffineTransformOp scaleOp =
new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
return scaleOp.filter((BufferedImage) image, res);
}
}
Your code updates the UI from a background thread, which is definitely not allowed. You need to ensure you update from the FX Application Thread. You also want to try to "throttle" the actual UI updates to occur no more than once per JavaFX frame rendering. The easiest way to do this is with an AnimationTimer, whose handle() method is invoked each time a frame is rendered.
Here's a version of your code which does that:
import java.awt.Dimension;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicReference;
import org.tc33.jheatchart.HeatChart;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
ImageView imageView = new ImageView();
final int scale = 15;
#Override
public void start(Stage primaryStage) {
AtomicReference<BufferedImage> image = new AtomicReference<>();
Thread generator = new Thread(() -> {
int col = 0;
LinkedList<Long> fps = new LinkedList<>();
while (true) {
fps.add(System.currentTimeMillis());
double[][] matrix = new double[48][128];
for (int i = 0; i < 48; i++) {
for (int j = 0; j < 128; j++) {
matrix[i][j] = col == j ? Math.random() : 0;
}
}
col = (col + 1) % 128;
HeatChart heatChart = new HeatChart(matrix, 0, 1);
heatChart.setShowXAxisValues(false);
heatChart.setShowYAxisValues(false);
heatChart.setLowValueColour(java.awt.Color.black);
heatChart.setHighValueColour(java.awt.Color.white);
heatChart.setAxisThickness(0);
heatChart.setChartMargin(0);
heatChart.setCellSize(new Dimension(1, 1));
long currentTime = System.currentTimeMillis();
fps.removeIf(elem -> currentTime - elem > 1000);
System.out.println(fps.size());
image.set((BufferedImage) scale(heatChart.getChartImage(), scale));
}
});
VBox box = new VBox();
box.getChildren().add(imageView);
Scene scene = new Scene(box, 1920, 720);
primaryStage.setScene(scene);
primaryStage.show();
generator.setDaemon(true);
generator.start();
AnimationTimer animation = new AnimationTimer() {
#Override
public void handle(long now) {
BufferedImage img = image.getAndSet(null);
if (img != null) {
imageView.setImage(SwingFXUtils.toFXImage(img, null));
}
}
};
animation.start();
}
public static void main(String[] args) {
launch(args);
}
private static Image scale(Image image, int scale) {
BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(scale, scale);
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
return scaleOp.filter((BufferedImage) image, res);
}
}
Using the AtomicReference to wrap the buffered image ensures that it is safely shared between the two threads.
On my machine, this generates about 130 images per second; note that not all are displayed, as only the latest one is shown each time the JavaFX graphics framework displays a frame (which is typically throttled at 60fps).
If you want to ensure you show all images that are generated, i.e. you throttle the image generation by the JavaFX framerate, then you can use a BlockingQueue to store the images:
// AtomicReference<BufferedImage> image = new AtomicReference<>();
// Size of the queue is a trade-off between memory consumption
// and smoothness (essentially works as a buffer size)
BlockingQueue<BufferedImage> image = new ArrayBlockingQueue<>(5);
// ...
// image.set((BufferedImage) scale(heatChart.getChartImage(), scale));
try {
image.put((BufferedImage) scale(heatChart.getChartImage(), scale));
} catch (InterruptedException exc) {
Thread.currentThread.interrupt();
}
and
#Override
public void handle(long now) {
BufferedImage img = image.poll();
if (img != null) {
imageView.setImage(SwingFXUtils.toFXImage(img, null));
}
}
The code is pretty inefficient, as you generate a new matrix, new HeatChart, etc, on every iteration. This causes many objects to be created on the heap and quickly discarded, which can cause the GC to be run too often, particularly on a small-memory machine. That said, I ran this with the maximum heap size set at 64MB, (-Xmx64m), and it still performed fine. You may be able to optimize the code, but using the AnimationTimer as shown above, generating images more quickly will not cause any additional stress on the JavaFX framework. I would recommend investigating using the mutability of HeatChart (i.e. setZValues()) to avoid creating too many objects, and/or using PixelBuffer to directly write data to the image view (this would need to be done on the FX Application Thread).
Here's a different example, which (almost) completely minimizes object creation, using one off-screen int[] array to compute data, and one on-screen int[] array to display it. There's a little low-level threading details to ensure the on-screen array is only seen in a consistent state. The on-screen array is used to underly a PixelBuffer, which in turn is used for a WritableImage.
This class generates the image data:
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
public class ImageGenerator {
private final int width;
private final int height;
// Keep two copies of the data: one which is not exposed
// that we modify on the fly during computation;
// another which we expose publicly.
// The publicly exposed one can be viewed only in a complete
// state if operations on it are synchronized on this object.
private final int[] privateData ;
private final int[] publicData ;
private final long[] frameTimes ;
private int currentFrameIndex ;
private final AtomicLong averageGenerationTime ;
private final ReentrantLock lock ;
private static final double TWO_PI = 2 * Math.PI;
private static final double PI_BY_TWELVE = Math.PI / 12; // 15 degrees
public ImageGenerator(int width, int height) {
super();
this.width = width;
this.height = height;
privateData = new int[width * height];
publicData = new int[width * height];
lock = new ReentrantLock();
this.frameTimes = new long[100];
this.averageGenerationTime = new AtomicLong();
}
public void generateImage(double angle) {
// compute in private data copy:
int minDim = Math.min(width, height);
int minR2 = minDim * minDim / 4;
for (int x = 0; x < width; x++) {
int xOff = x - width / 2;
int xOff2 = xOff * xOff;
for (int y = 0; y < height; y++) {
int index = x + y * width;
int yOff = y - height / 2;
int yOff2 = yOff * yOff;
int r2 = xOff2 + yOff2;
if (r2 > minR2) {
privateData[index] = 0xffffffff; // white
} else {
double theta = Math.atan2(yOff, xOff);
double delta = Math.abs(theta - angle);
if (delta > TWO_PI - PI_BY_TWELVE) {
delta = TWO_PI - delta;
}
if (delta < PI_BY_TWELVE) {
int green = (int) (255 * (1 - delta / PI_BY_TWELVE));
privateData[index] = (0xff << 24) | (green << 8); // green, fading away from center
} else {
privateData[index] = 0xff << 24; // black
}
}
}
}
// copy computed data to public data copy:
lock.lock();
try {
System.arraycopy(privateData, 0, publicData, 0, privateData.length);
} finally {
lock.unlock();
}
frameTimes[currentFrameIndex] = System.nanoTime() ;
int nextIndex = (currentFrameIndex + 1) % frameTimes.length ;
if (frameTimes[nextIndex] > 0) {
averageGenerationTime.set((frameTimes[currentFrameIndex] - frameTimes[nextIndex]) / frameTimes.length);
}
currentFrameIndex = nextIndex ;
}
public void consumeData(Consumer<int[]> consumer) {
lock.lock();
try {
consumer.accept(publicData);
} finally {
lock.unlock();
}
}
public long getAverageGenerationTime() {
return averageGenerationTime.get() ;
}
}
And here's the UI:
import java.nio.IntBuffer;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AnimationApp extends Application {
private final int size = 400 ;
private IntBuffer buffer ;
#Override
public void start(Stage primaryStage) throws Exception {
// background image data generation:
ImageGenerator generator = new ImageGenerator(size, size);
// Generate new image data as fast as possible:
Thread thread = new Thread(() -> {
while( true ) {
long now = System.currentTimeMillis() ;
double angle = 2 * Math.PI * (now % 10000) / 10000 - Math.PI;
generator.generateImage(angle);
}
});
thread.setDaemon(true);
thread.start();
generator.consumeData(data -> buffer = IntBuffer.wrap(data));
PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
WritableImage image = new WritableImage(pixelBuffer);
BorderPane root = new BorderPane(new ImageView(image));
Label fps = new Label("FPS: ");
root.setTop(fps);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle("Give me a ping, Vasili. ");
primaryStage.show();
AnimationTimer animation = new AnimationTimer() {
#Override
public void handle(long now) {
// Update image, ensuring we only see the underlying
// data in a consistent state:
generator.consumeData(data -> {
pixelBuffer.updateBuffer(pb -> null);
});
long aveGenTime = generator.getAverageGenerationTime() ;
if (aveGenTime > 0) {
double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
fps.setText(String.format("FPS: %.2f", aveFPS));
}
}
};
animation.start();
}
public static void main(String[] args) {
Application.launch(args);
}
}
For a version that doesn't rely on the JavaFX 13 PixelBuffer, you can just modify this class to use a PixelWriter (AIUI this won't be quite as efficient, but works just as smoothly in this example):
// generator.consumeData(data -> buffer = IntBuffer.wrap(data));
PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
// PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
// WritableImage image = new WritableImage(pixelBuffer);
WritableImage image = new WritableImage(size, size);
PixelWriter pixelWriter = image.getPixelWriter() ;
and
AnimationTimer animation = new AnimationTimer() {
#Override
public void handle(long now) {
// Update image, ensuring we only see the underlying
// data in a consistent state:
generator.consumeData(data -> {
// pixelBuffer.updateBuffer(pb -> null);
pixelWriter.setPixels(0, 0, size, size, format, data, 0, size);
});
long aveGenTime = generator.getAverageGenerationTime() ;
if (aveGenTime > 0) {
double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
fps.setText(String.format("FPS: %.2f", aveFPS));
}
}
};
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.
Im making project for my uni, and I have a problem with one thing - everything works well, but there is bug with comparing colors of two pixels.
I have to count area of some figure, and I have to use MonteCarlo method. ( generate random points, count points in figure and out, calculete figure area )
And some points are counted well, some dont, I have no idea whats wrong, Im trying to solve that few hours...
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.swing.*;
public class Runner extends JApplet{
private BufferedImage img;
public ArrayList<Point> w;
public ArrayList<Point> poza;
public BufferedImage output;
public void init(){
try{
img = ImageIO.read(new File("figura.gif"));
} catch (IOException e){
e.getStackTrace();
System.err.println("Nie ma obrazkaXD");
}
}
public void paint(Graphics g){
w = new ArrayList<Point>();
poza = new ArrayList<>();
super.paint(g);
Random random = new Random();
int wys = img.getHeight();
int szer = img.getWidth();
g.drawImage(img, 0, 0, wys, szer, null);
for (int i = 0; i < 1000; i++) {
int x = random.nextInt(wys);
int y = random.nextInt(szer);
Point p = new Point(x,y);
g.setColor(Color.GREEN);
g.drawOval(y, x, 1, 1);
Color c = new Color(img.getRGB(y, x));;
if(c.equals(Color.BLACK)){
w.add(p);
g.setColor(Color.RED);
g.drawOval(y, x, 1, 1);
}else{
poza.add(p);
}
}
float a = w.size();
float b = poza.size()+w.size();
float poleProstokata = wys*szer;
float pole = a/b*poleProstokata;
}
I would suggest you to switch the y and y Coordinate, because as described in the Oracle Documentation :
https://docs.oracle.com/javase/7/docs/api/java/awt/image/BufferedImage.html#getRGB(int,%20int)
The method getRGB takes the X first and the Y second, so you'll have to use
Color c = new Color(img.getRGB(x, y));
instead of
Color c = new Color(img.getRGB(y, x));;
and why dont use an int ? I mean, you always convert the returned int from getRGB to a color and compare it. But why dont create a int from the Color Black and compare it to the int returned from getRGB(x,y) ?
Heres what I suggest :
int black=Color.BLACK.getRGB();
at the beginning of your paint method
and in your loop :
int c=img.getRGB(y, x);
and compare them :
if (black==c) {
//Do your stuff...
}
There is something in the following code that I am unable to understand. After digging through google for a while, I decided it would be better to ask someone.
I am following a game programming tutorial on youtube, and I feel I understand (to some degree) everything I have written, except for some lines which concern the rendering part of the program.
package com.thomas.game;
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import javax.swing.JFrame;
import com.thomas.game.graphics.Screen;
import com.thomas.game.input.Keyboard;
public class Game extends Canvas implements Runnable {
private static final int WIDTH = 300;
private static final int HEIGHT = (WIDTH / 16) * 9;
private static final int SCALE = 3;
private static final String TITLE = "Game";
private JFrame frame;
private Thread thread;
private Screen screen;
private BufferedImage image;
private Keyboard key;
private int[] pixels;
private boolean running = false;
private int x = 0, y = 0;
public Game() {
setPreferredSize(new Dimension(WIDTH * SCALE, HEIGHT * SCALE));
screen = new Screen(WIDTH, HEIGHT);
frame = new JFrame();
initializeFrame();
image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
this is what I don't understand. I get the image raster, from which I get the databuffer. I typecast that databuffer into a (DatabufferInt), which allows me to retrieve an int[] through the getData() method. After this is done, pixel.length has a value of 48600, and every index contains the int value 0. Operating with this int[] makes the program render like it is supposed to. However, if I don't typecast and retrieve the int[] in the above manner, and instead say pixels = new int[48600], i end up with a black screen.
I guess what I want to know is: what is the difference between these two int[], or rather, what makes the first one work? How does it work?
key = new Keyboard();
addKeyListener(key);
setFocusable(true);
}
public void run() {
long lastTime = System.nanoTime();
double nsPerTick = 1E9/60;
double delta = 0;
long now;
int ticks = 0;
int frames = 0;
long timer = System.currentTimeMillis();
while(running) {
now = System.nanoTime();
delta += (now - lastTime) / nsPerTick;
lastTime = now;
while(delta >= 1) {
tick();
ticks++;
delta--;
}
render();
frames++;
if(System.currentTimeMillis() - timer >= 1000) {
timer += 1000;
frame.setTitle(TITLE + " | ups: " + ticks + " fps: " + frames);
ticks = 0;
frames = 0;
}
}
}
private void render() {
BufferStrategy bs = getBufferStrategy(); // retrieves the bufferstrategy from the current component (the instance of Game that calls this method)
if(bs == null) {
createBufferStrategy(3);
return;
}
screen.clear();
screen.render(x, y);
getPixels();
Graphics g = bs.getDrawGraphics(); // retrieves a graphics object from the next in line buffer in the bufferstrategy, this graphics object draws to that buffer
g.drawImage(image, 0, 0, getWidth(), getHeight(), null); // draws the bufferedimage to the available buffer
g.dispose();
bs.show(); // orders the next in line buffer (which the graphics object g is tied to) to show its contents on the canvas
}
private void tick() {
key.update();
if(key.up)
y--;
if(key.down)
y++;
if(key.left)
x--;
if(key.right)
x++;
}
public void initializeFrame() {
frame.setTitle(TITLE);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.add(this);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public synchronized void start() {
running = true;
thread = new Thread(this);
thread.start();
}
public synchronized void stop() {
running = false;
try {
thread.join();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Game game = new Game();
game.start();
}
public void getPixels() {
for(int i = 0; i < pixels.length; i++)
pixels[i] = screen.pixels[i];
}
}
It seems like the bufferedimage gets values from the pixels array. But I don't understand how these two communicate, or how they are connected. I haven't explicitly told the bufferedimage to get its pixels from the pixels array, so how does it know?
I will also attach the Screen class, which is responsible for updating the pixels array.
package com.thomas.game.graphics;
import java.util.Random;
public class Screen {
private int width, height;
public int[] pixels;
private final int MAP_SIZE = 64;
private final int MAP_SIZE_MASK = MAP_SIZE - 1;
private int[] tiles;
private int tileIndex;
private int xx, yy;
private Random r;
public Screen(int w, int h) {
width = w;
height = h;
pixels = new int[width * height];
tiles = new int[MAP_SIZE * MAP_SIZE];
r = new Random(0xffffff);
for(int i = 0; i < tiles.length; i++) {
tiles[i] = r.nextInt();
}
tiles[0] = 0;
}
public void clear() {
for(int i = 0; i < pixels.length; i++)
pixels[i] = 0;
}
public void render(int xOffset, int yOffset) {
for(int y = 0; y < height; y++) {
yy = y + yOffset;
for(int x = 0; x < width; x++) {
xx = x + xOffset;
tileIndex = (yy >> 4 & MAP_SIZE_MASK) * MAP_SIZE + (xx >> 4 & MAP_SIZE_MASK);
pixels[y * width + x] = tiles[tileIndex];
}
}
}
}
I really hope someone can explain this to me, it would be greatly appreciated. The program is working like it is supposed to, but I don't feel comfortable continuing on the tutorial until I grasp this.
Basic types like short, int, long etc are not Objects.
However, int[] is an array. Arrays are objects in java. Java manipulates objects by reference, not value.
In this line you are not creating a new object. You are storing a reference to the object int[] in your variable pixels. Anything you change in pixels, gets changed inside of the int[] object in image:
pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
I've created an example, try running this code:
public class Data {
private int[] values = {25,14};
public int[] getValues() {
return values;
}
public static void main(String[] args) {
Data d = new Data();
System.out.println(d.getValues()[0]);
int[] values = d.getValues();
values[0] = 15;
System.out.println(d.getValues()[0]);
}
}
Output:
25
15
Note that you have this code...
pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
while it should be like this...
pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData();
Change image to img.
Hope it works!