JavaFX Scaling differently sized nodes to the same size - javafx

I am drawing differently sized maps on a pane. Some look decent, others are just presented as a small shape and you have to zoom in to get it to the right size. I want those maps to appear roughly the same size each time I initialize (so I don't have to manually scale each map). I've got Point2D points for the min and max values of x and y of the pane they're drawn on and same goes for the map (which is a Group of polygons). How do I set the distance between, say, the minPoint of Pane and the minPoint of Group? Or am I approaching this the wrong way?
edit:
public void setDistance(Group map, Point2D paneSize, Point2D mapSize){
//um diese distance verschieben, if distance > 10px (scale)
double d = paneSize.distance(mapSize);
double scale = ??
map.setScaleX(scale);
map.setScaleY(scale);
}
That's how I planned on doing it, not sure about that one line though.

To scale the node to the size of the parent node, the difference in the size is not important. What is important is the quotient of the sizes, more precisely the minimum of the quotients of the heights and the widths (assuming you want to fill the parent in one direction completely).
Example:
#Override
public void start(Stage primaryStage) {
Text text = new Text("Hello World!");
Pane root = new Pane();
root.getChildren().add(text);
InvalidationListener listener = o -> {
Bounds rootBounds = root.getLayoutBounds();
Bounds elementBounds = text.getLayoutBounds();
double scale = Math.min(rootBounds.getWidth() / elementBounds.getWidth(),
rootBounds.getHeight() / elementBounds.getHeight());
text.setScaleX(scale);
text.setScaleY(scale);
// center the element
elementBounds = text.getBoundsInParent();
double cx = (elementBounds.getMinX() + elementBounds.getMaxX()) / 2;
double cy = (elementBounds.getMinY() + elementBounds.getMaxY()) / 2;
text.setTranslateX(rootBounds.getWidth() / 2 - cx + text.getTranslateX());
text.setTranslateY(rootBounds.getHeight() / 2 - cy + text.getTranslateY());
};
root.layoutBoundsProperty().addListener(listener);
text.layoutBoundsProperty().addListener(listener);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}

Related

Drawing the "Seed of Life" without redrawing anything

I have a mildly interesting problem which I can't quite figure out (although in fairness, I am pretty drunk)
The "Seed of Life" is a pattern created from drawing circles of equal radius, centred on the intersection of the previous circle.
Language doesn't really matter, the theory is more important here. Anything which can draw a circle will do it. For example, HTML5 + JS canvas can do it. It's a lovely example of how recursion can help solve problems.
The problem is that a naive approach will end up redrawing many, many circles. With 7 layers, you'll end up with over 300,000 circle draws.
A simple approach is to maintain a list of previous circle centre points, and only draw circles which are not in that list.
My question is whether there's a "better" way to approach this? Something which doesn't require checking that list.
A fun problem to ponder.
I think I have this solved thanks to a friend. I'll post here what I'm doing now in case someone ever is curious.
In short, starting from the center and working out, calculate the vertices of a hexagon, and subdivide each edge of the hexagon into i number of places, where i is the layer number.
I drew it in C# using SkiaSharp, but the code is nothing special to the language, there's no reason this couldn't be written in any language. Here's the significant bits:
const float seedAngle = (float)(Math.PI / 3.0);
static void SeedOfLifeDemo(int x, int y) {
//setting up Skia stuff, this will be different depending what language you're using.
var info = new SKImageInfo(x, y);
using var bitmap = FlatImage(info, SKColors.White);
SKCanvas canvas = new SKCanvas(bitmap);
float radius = Math.Min(x, y) / 15;
SKPoint center = new SKPoint(x / 2f, y / 2f);
SKPaint strokePaint = new SKPaint {
Color = SKColors.Black,
Style = SKPaintStyle.Stroke,
StrokeWidth = 1,
IsAntialias = true,
};
int layers = 4;
//Draw the very central circle. This is just a little easier than adding that edge case to SubdividedHexagonAboutPoint
canvas.DrawCircle(center, radius, strokePaint);
for (int i = 1; i <= layers; i++) {
foreach (SKPoint p in SubdividedHexagonAboutPoint(center, radius * i, i)) {
canvas.DrawCircle(p, radius, strokePaint);
}
}
SaveImage(bitmap, "SeedOfLifeFastDemo.Jpg");//More Skia specific stuff
}
//The magic!
static List<SKPoint> SubdividedHexagonAboutPoint(SKPoint centre, float radius, int subdivisions) {
List<SKPoint> points = new List<SKPoint>(6 * subdivisions);
SKPoint? prevPoint = null;
for (int i = 0; i < 7; i++) {//Step around the circle. The 7th step is to close the last edge
float x = (float)(Math.Sin(seedAngle * i) * radius + centre.X);
float y = (float)(Math.Cos(seedAngle * i) * radius + centre.Y);
SKPoint point = new SKPoint(x, y);
if (prevPoint != null) {
points.Add(point);//include the "primary" 6 points
if (subdivisions > 0) {
float xDist = (point.X - prevPoint.Value.X) / subdivisions;
float yDist = (point.Y - prevPoint.Value.Y) / subdivisions;
for (int sub = 1; sub < subdivisions; sub++) {
SKPoint subPoint = new SKPoint(point.X - xDist * sub, point.Y - yDist * sub);
points.Add(subPoint);//include the edge subdivisions
}
}
}
prevPoint = point;
}
return points;
}
This is quite an interesting exercise really, and another example of where recursion can really bite you when used badly.

Drawing shape with open lines in Java(f)x Canvas

Is it possible to draw a shape with open ends?
E.g.: Let's say I want to draw a tree, which roots are open. Is there a elegant way to let the ends open, without overdrawing the already drawed lines?
I could overdraw it with shapes, which are exactly as big as my openings and have the color of the background, but I don't think that is the elegant way and I don't find any option to let them open. Perhaps I'm just blind and I could make strokePolygon(...) in which not all points are linked, but I think that's neither the way to go.
Let's have a simple shape:
[ceate Scene and Stage, etc]
Canvas sc = new Canvas(x, y);
GraphicsContext gcCs = cs.getGraphicsContext2D();
gcCs.setStroke(Color.BLACK);
double counter = 0.0;
[calculate points, instantiate arrays, etc]
for (int i = 0; i < arrayX.length; i++)
{
arrayX = shapeMidX + Math.cos(Math.toRadiants(counter * Math.PI)) * shapeSizeX / 2);
arrayY = shapeMidY + Math.sin(Math.toRadiants(counter * Math.PI)) * shapeSizeY / 2);
}
gcCs.strokePolygon(arrayX, arrayY, arrayX.length);
[making other things]
stackPane.getChildren().add(sc);
I know that I could use .strokeOval(), but I wanted to have a example that is near of my own code.
I like to draw my shapes from the center.
P.S.: I wrote the for() { } out of my head, it could be that there's something wrong. I've got no Internet at home at the moment, so my answers could be taking a lot of time.
Thank you in advance.
You could draw individual lines using strokeLine and store the current position in variables allowing you to draw any combination of lines.
You could also construct a path instead which allows you to use moveTo instead of lineTo to "skip" a segment. This way you don't need to keep track of the previous position for continuous lines.
The following example draws every other line of a square this way:
#Override
public void start(Stage primaryStage) {
Canvas canvas = new Canvas(400, 400);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.moveTo(100, 100);
gc.lineTo(100, 300);
gc.moveTo(300, 300);
gc.lineTo(300, 100);
// gc.moveTo(100, 100);
gc.stroke();
Scene scene = new Scene(new StackPane(canvas));
primaryStage.setScene(scene);
primaryStage.show();
}

Gmap.net show only markers within polygon

I am currently working with gmap.net to create a certain radius with a polygon. I currently have made a polygon for the radius but now I come to the problem that I want to create multipule markers but only show the markers who are inside the polygon. Is this possible?
_polygonOverlay = new GMapOverlay("destination");
_gMap.Overlays.Add(_polygonOverlay);
private void CreateCircle(PointLatLng destination, double radius)
{
List<PointLatLng> radiusPoint = new List<PointLatLng>();
double seg = Math.PI * 2 / 40;
for (int i = 0; i < 40; i++)
{
double theta = seg * i;
double latitude = destination.Lat + Math.Cos(theta) * radius;
double longitude = destination.Lng + Math.Sin(theta) * radius;
PointLatLng cirlePoint = new PointLatLng(latitude, longitude);
radiusPoint.Add(cirlePoint);
}
GMapPolygon radiusCircle = new GMapPolygon(radiusPoint, "radius");
_polygonOverlay.Polygons.Add(radiusCircle);
}
private void CreateMarkers()
{
_polygonOverlay.Markers.Add(new GMarkerGoogle(new PointLatLng(xxx, xxx), GMarkerGoogleType.blue));
_polygonOverlay.Markers.Add(new GMarkerGoogle(new PointLatLng(xxx, xxx), GMarkerGoogleType.blue));
_polygonOverlay.Markers.Add(new GMarkerGoogle(new PointLatLng(xxx, xxx), GMarkerGoogleType.blue));
}
Here is a little sample of the code I have that create a circle (still needs some work on it) and some markers.
Already thanks is advance
Since you are dealing with a circle, you should be able to simply check the distance of your marker from the center of the circle. If the distance is greater than the radius, don't add it to the overlay.
GMap gives you access to the necessary methods to determine this information. Do something like this:
//Assuming p1 is your marker and p2 is your circle center coordinate
double markerDist = GMap.NET.MapProviders.EmptyProvider.Instance.Projection.GetDistance(p1.Position, p2);
if(markerDist <= circleRadius)
{
//Add the marker to the overlay
}
Assume you have a GMapPolygon with some Points, you could just use
bool inside = gMapPolygon.IsInside(point)
to check if the point of a GMarker is inside that GMapPolygon

Processing: Draw vector instead of pixels

I have a simple Processing Sketch, drawing a continuous line of ellipses with a 20px diameter. Is there a way to modify the sketch so that it draws vector shapes instead of pixels?
void setup() {
size(900, 900);
background(110, 255, 94);
}
void draw() {
ellipse(mouseX, mouseY, 20, 20);
fill(255);
}
Thanks to everyone who can provide some helpful advice.
Expanding my comment above, there a couple of things to tackle:
drawing a continuous line of ellipses with a 20px diameter
draws vector shapes
Currently you're drawing ellipses based on mouse movement.
A side effect is that if you move the mouse fast enough you will have gaps in between ellipses.
To fill the gaps you can work out the distance between every two ellipses.
If the distance is greater than the sizes of these two ellipses you can draw some in between.
The PVector class provides a lerp() function that allows you easily interpolate between two points.
You can read more on this and run some examples here
Using the ratio between these distance of two points and the ellipse size the number of points needed in between.
Here is an example that stores mouse locations to a list of PVectors as you drag the mouse:
//create an array list to store points to draw
ArrayList<PVector> path = new ArrayList<PVector>();
//size of each ellipse
float size = 20;
//how tight will the extra ellipses be drawn together
float tightness = 1.25;
void setup() {
size(900, 900);
}
void draw() {
background(110, 255, 94);
fill(255);
//for each point in the path, starting at 1 (not 0)
for(int i = 1; i < path.size(); i++){
//get a reference to the current and previous point
PVector current = path.get(i);
PVector previous = path.get(i-1);
//calculate the distance between them
float distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
int extraPoints = (int)(round(distance/size * tightness));
//draw the previous point
ellipse(previous.x,previous.y,size,size);
//if there are any exta points to be added, compute and draw them:
for(int j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
float interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
PVector inbetween = PVector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,size,size);
}
}
//draw instructions
fill(0);
text("SPACE = clear\nLEFT = decrease tightness\nRIGHT = increase tightness\ntightness:"+tightness,10,15);
}
void mouseDragged(){
path.add(new PVector(mouseX,mouseY));
}
void keyPressed(){
if(keyCode == LEFT) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == RIGHT) tightness = constrain(tightness+0.1,0.0,3.0);
if(key == ' ') path.clear();
}
Note that the interpolation between points is linear.
It's the simplest, but as the name implies, it's all about lines:
it always connects two points in a straight line, not curves.
I've added the option to control how tight interpolated ellipses will be packed together. Here are a couple of screenshots with different tightness levels. You'll notice as tightness increases, the lines will become more evident:
You run the code bellow:
//create an array list to store points to draw
var path = [];
//size of each ellipse
var ellipseSize = 20;
//how tight will the extra ellipses be drawn together
var tightness = 1.25;
function setup() {
createCanvas(900, 900);
}
function draw() {
background(110, 255, 94);
fill(255);
//for each point in the path, starting at 1 (not 0)
for(var i = 1; i < path.length; i++){
//get a reference to the current and previous point
var current = path[i];
var previous = path[i-1];
//calculate the distance between them
var distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
var extraPoints = round(distance/ellipseSize * tightness);
//draw the previous point
ellipse(previous.x,previous.y,ellipseSize,ellipseSize);
//if there are any exta points to be added, compute and draw them:
for(var j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
var interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
var inbetween = p5.Vector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,ellipseSize,ellipseSize);
}
}
//draw instructions
fill(0);
text("BACKSPACE = clear\n- = decrease tightness\n+ = increase tightness\ntightness:"+tightness,10,15);
}
function mouseDragged(){
path.push(createVector(mouseX,mouseY));
}
function keyPressed(){
if(keyCode == 189) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == 187) tightness = constrain(tightness+0.1,0.0,3.0);
if(keyCode == BACKSPACE) path = [];
}
//https://stackoverflow.com/questions/40673192/processing-draw-vector-instead-of-pixels
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js"></script>
If you want smoother lines you will need to use a different interpolation such as quadratic or cubic interpolation. You can start with existing Processing functions for drawing curves such as curve() or bezier(),and you'll find some helpful resources unrelated to Processing here,here and here.
On vector shapes
You're not directly working with pixels[], you're drawing shapes.
These shapes can easily be saved to PDF using Processing's PDF library
Check out the Single Frame from an Animation (With Screen Display) example.
Here is a version that saves to PDF when pressing the 's' key:
import processing.pdf.*;
//create an array list to store points to draw
ArrayList<PVector> path = new ArrayList<PVector>();
//size of each ellipse
float size = 20;
//how tight will the extra ellipses be drawn together
float tightness = 1.25;
//PDF saving
boolean record;
void setup() {
size(900, 900);
}
void draw() {
background(110, 255, 94);
fill(255);
//if we need to save the current frame to pdf, begin recording drawing instructions
if (record) {
// Note that #### will be replaced with the frame number. Fancy!
beginRecord(PDF, "frame-####.pdf");
}
//for each point in the path, starting at 1 (not 0)
for(int i = 1; i < path.size(); i++){
//get a reference to the current and previous point
PVector current = path.get(i);
PVector previous = path.get(i-1);
//calculate the distance between them
float distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
int extraPoints = (int)(round(distance/size * tightness));
//draw the previous point
ellipse(previous.x,previous.y,size,size);
//if there are any exta points to be added, compute and draw them:
for(int j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
float interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
PVector inbetween = PVector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,size,size);
}
}
//once what we want to save has been recorded to PDF, stop recording (this will skip saving the instructions text);
if (record) {
endRecord();
record = false;
println("pdf saved");
}
//draw instructions
fill(0);
text("SPACE = clear\nLEFT = decrease tightness\nRIGHT = increase tightness\ntightness:"+tightness+"\n's' = save PDF",10,15);
}
void mouseDragged(){
path.add(new PVector(mouseX,mouseY));
}
void keyPressed(){
if(keyCode == LEFT) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == RIGHT) tightness = constrain(tightness+0.1,0.0,3.0);
if(key == ' ') path.clear();
if(key == 's') record = true;
}
In addition to George's great answer (which I've +1'd), I wanted to offer a more basic option:
The problem, like George said, is that when you move the mouse, you actually skip over a bunch of pixels. So if you only draw ellipses or points at mouseX, mouseY then you'll end up with gaps.
The dumb fix: the pmouseX and pmouseY variables hold the previous position of the cursor.
That might not sound very useful, but they allow you to solve exactly your problem. Instead of drawing ellipses or points at the current mouse position, draw a line from the previous position to the current position. This will eliminate any gaps in your lines.
void draw(){
line(pmouseX, pmouseY, mouseX, mouseY);
}
Shameless self-promotion: I've written a tutorial on getting user input in Processing available here.
Note: This dumb solution will only work if you aren't redrawing the background every frame. If you need to redraw everything every frame, then George's answer is the way to go.

JavaFX scaling a group of shapes without affecting stroke width

I have a custom component that manages a set of shape nodes (path).
I also need to be able to horizonatal zoom and pan, so I thought I'd put all the shapes in a group and scale/translate the X axis.
However, I do not want the stroke width to scale, only the size of the shapes.
How do I make only the geometry scale? I could post some code but it is pretty much the same zooming code as this post but using different shapes.
JavaFX correct scaling
Any way to turn off stroke scaling? Change scaling behavior?
I had a hard time following the answer in the link provided, so I give my solution which I think is a bit easier to understand. The problem is, if you scale a node the width of the stroke also scale's and you get these big fat lines for the shape. The solution that I use is that I apply a scaling transform to the node, and I also descale the width of the stroke. I'll
show the code for two different scenarios: 1.) I used an SVGPath node and show how to scale the svg without scaling the stroke line, and 2.) I'll do the same thing using the GraphicsContent on a canvas. Here's the code snippet
private void start() {
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
double originalWidth, originalHeight, scaleX, scaleY;
// here's scaling using SVGPath Node type
SVGPath svgPath = new SVGPath();
svgPath.setFill(Color.TRANSPARENT);
//svgPath.setStrokeWidth(0);
svgPath.setStroke(Color.BLACK);
svgPath.setContent("M 0 0 L 6 0 L 6 5.5 A.5,.5 0 0 1 5.5 6 L0,6 L0,0");
// compute scale ratio
originalWidth = svgPath.prefWidth(-1);
originalHeight = svgPath.prefHeight(originalWidth);
scaleX = canvas1Width / originalWidth;
scaleY = canvas1Height / originalHeight;
// scale the svgPath; adjust the stroke width
svgPath.setScaleX(scaleX);
svgPath.setScaleY(scaleY);
svgPath.setStrokeWidth(1.0 / scaleX);
canvasGroup1.getChildren().addAll(svgPath);
// here's scaling using the Canvas / GraphicsContext
GraphicsContext gc = canvas2.getGraphicsContext2D();
double strokeWidth = gc.getLineWidth();
originalHeight =6;
originalWidth = 6;
scaleX = canvas2Width / originalWidth;
scaleY = canvas2Height / originalHeight;
Affine affine = new Affine();
affine.appendScale(scaleX, scaleY);
gc.setTransform(affine);
// the scaling must be set before the appendSVGPath statement
// the SVGPath will not scale if apply the transform after the appendSVGPath
gc.appendSVGPath("M 0 0 L 6 0 L 6 5.5 A.5,.5 0 0 1 5.5 6 L0,6 L0,0");
gc.setLineWidth(strokeWidth/scaleX);
gc.stroke();
Both scenarios work, but I will mention that there are two cons to using the GraphicsContext approach. First, the GraphicsContext approach doesn't produce as smooth of an arc as using the SVGPath. Lines are fine. Secondly, you can't easily get the bounds of the appendSVGPath to compute your scaling factor. As you may notice, I hard wired them in for that scenario.

Resources