I have the following test code, where I try to clip a MeshView with a circle.
I also tried putting the meshView into a group then clipping that, but this result in a black circle.
Is there a way to clip a MeshView, preferably without putting it into a group?
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.scene.image.Image
import scalafx.scene.paint.{Color, PhongMaterial}
import scalafx.scene.shape.{TriangleMesh, Circle, MeshView}
import scalafx.scene.{Group, PerspectiveCamera, Scene, SceneAntialiasing}
object Test4 extends JFXApp {
stage = new PrimaryStage {
scene = new Scene(500, 500, true, SceneAntialiasing.Balanced) {
fill = Color.LightGray
val clipCircle = Circle(150.0)
val meshView = new MeshView(new RectangleMesh(500,500)) {
// takes a while to load
material = new PhongMaterial(Color.White, new Image("https://peach.blender.org/wp-content/uploads/bbb-splash.png"), null, null, null)
}
// val meshGroup = new Group(meshView)
meshView.setClip(clipCircle)
root = new Group {children = meshView; translateX = 250.0; translateY = 250.0; translateZ = 560.0}
camera = new PerspectiveCamera(false)
}
}
}
class RectangleMesh(Width: Float, Height: Float) extends TriangleMesh {
points = Array(
-Width / 2, Height / 2, 0,
-Width / 2, -Height / 2, 0,
Width / 2, Height / 2, 0,
Width / 2, -Height / 2, 0
)
texCoords = Array(
1, 1,
1, 0,
0, 1,
0, 0
)
faces = Array(
2, 2, 1, 1, 0, 0,
2, 2, 3, 3, 1, 1
)
The clippling actually works fine over the MeshView wrapped around a Group.
If you check JavaDoc for setClip():
There is a known limitation of mixing Clip with a 3D Transform. Clipping is essentially a 2D image operation. The result of a Clip set on a Group node with 3D transformed children will cause its children to be rendered in order without Z-buffering applied between those children.
As a result of this:
Group meshGroup = new Group(meshView);
meshGroup.setClip(clipCircle);
you will have a 2D image, and it seems Material is not applied. However you can check there's a mesh, by seting this:
meshView.setDrawMode(DrawMode.LINE);
So in your case, adjusting dimensions:
#Override
public void start(Stage primaryStage) {
Circle clipCircle = new Circle(220.0);
MeshView meshView = new MeshView(new RectangleMesh(400,400));
meshView.setDrawMode(DrawMode.LINE);
Group meshGroup = new Group(meshView);
meshGroup.setClip(clipCircle);
PerspectiveCamera camera = new PerspectiveCamera(false);
StackPane root = new StackPane();
final Circle circle = new Circle(220.0);
circle.setFill(Color.TRANSPARENT);
circle.setStroke(Color.RED);
root.getChildren().addAll(meshGroup, circle);
Scene scene = new Scene(root, 500, 500, true, SceneAntialiasing.BALANCED);
scene.setCamera(camera);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
will give this:
In the end, clipping doesn't make sense with 3D shapes. For that you can use just 2D shape to get the result you want.
If you want 3D clipping have a look at CSG operations. Check this question for a JavaFX based solution.
Related
I’m a bit new to working with 3D space and rotation. I have a list of four Vector3 points that represent the corners of a rectangle. What I want to do is use those points to create a box mesh that is rotated to exactly match the angle of rotation of the points.
Here is a babylonjs playground demo showing what I mean. In it you can see I’ve drawn a simple line mesh between the points. That is great and the rectangle drawn is at the expected angle given the data. I’ve also created a box mesh and configured its dimensions to match and placed its center point in the proper center of the points. So far so good, however I cannot figure out how to rotate the box so that it’s top face is parallel with the face of the rectangle.
https://playground.babylonjs.com/#SN5K8L#2
var createScene = function () {
// This creates a basic Babylon Scene object (non-mesh)
var scene = new BABYLON.Scene(engine);
// This creates and positions a free camera (non-mesh)
var target = new BABYLON.Vector3(1.5, 4, 0)
var camera = new BABYLON.ArcRotateCamera("camera1", Math.PI / 2 + Math.PI, Math.PI / 4, 10, target, scene)
// This attaches the camera to the canvas
camera.attachControl(canvas, true);
// This creates a light, aiming 0,1,0 - to the sky (non-mesh)
var light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
// Default intensity is 1. Let's dim the light a small amount
light.intensity = 0.7;
const axes = new BABYLON.AxesViewer(scene)
const points = [
new BABYLON.Vector3(1, 5, 1),
new BABYLON.Vector3(2, 5, 1),
new BABYLON.Vector3(2, 3, -1),
new BABYLON.Vector3(1, 3, -1)
]
const lines = BABYLON.MeshBuilder.CreateLines("lines", {
points: [...points, points[0]] // add a duplicate of first point to close polygon
}, scene)
const centerPoint = new BABYLON.Vector3(
(points[0].x + points[1].x + points[2].x + points[3].x) / 4,
(points[0].y + points[1].y + points[2].y + points[3].y) / 4,
(points[0].z + points[1].z + points[2].z + points[3].z) / 4
)
const width = Math.sqrt(
Math.pow(points[1].x - points[0].x, 2) +
Math.pow(points[1].y - points[0].y, 2) +
Math.pow(points[1].z - points[0].z, 2)
)
const depth = Math.sqrt(
Math.pow(points[2].x - points[1].x, 2) +
Math.pow(points[2].y - points[1].y, 2) +
Math.pow(points[2].z - points[1].z, 2)
)
const height = 0.15
const box = BABYLON.CreateBox("box", { width, height, depth}, scene)
box.position = centerPoint
//box.rotation = ???
return scene;
};
You can use Vector3.RotationFromAxis to compute the required rotation in Euler angles:
const rotationAxisX = points[1].subtract(points[0])
const rotationAxisZ = points[1].subtract(points[2])
const rotationAxisY = rotationAxisZ.cross(rotationAxisX)
// RotationFromAxis has the side effect of normalising the input vectors
// so retrieve the box dimensions here
const width = rotationAxisX.length()
const depth = rotationAxisZ.length()
const height = 0.15
const rotationEuler = BABYLON.Vector3.RotationFromAxis(
rotationAxisX,
rotationAxisY,
rotationAxisZ
)
const box = BABYLON.CreateBox("box", { width, height, depth}, scene)
box.position = centerPoint
box.rotation = rotationEuler
In my main application I have some SVGPaths that I add to an XYChart. Sometimes they have an ImagePattern fill which now needs to have a LinearGradient fill. The ImagePattern fill is a crosshatch and this needs to be colored with the LinearGradient the same as if it was a solid Rectangle with a LinearGradient applied. The SVGPath also has a dotted outline and the LinearGradient should fill the dotted outline and the ImagePattern fill as it they were part of the same shape.
I've written some sample code to show where I'm at. This colors the crosshatch as it's created and looks ok but isn't the effect I describe above as each cross in the ImagePattern has the LinearGradient applied individually. Ideally the LinearGradient would just be applied to the final SVGPath once the ImagePattern fill has been applied.
I've also tried some effects using Blend and ColorInput but haven't managed to get any closer to the solution.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.ImagePattern;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Line;
import javafx.scene.shape.SVGPath;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
List<Color> colors = Arrays.asList(Color.RED, Color.YELLOW, Color.GREEN);
ArrayList<Stop> stops = new ArrayList<>(colors.size() * 2);
for (int i = 0; i < colors.size(); i++) {
stops.add(new Stop(getOffset(i, colors.size()), colors.get(i)));
stops.add(new Stop(getOffset(i + 1, colors.size()), colors.get(i)));
}
LinearGradient lg = new LinearGradient(0, 0, 20, 20, false, CycleMethod.REPEAT, stops);
SVGPath svgPath = new SVGPath();
svgPath.setContent("M-84.1487,-15.8513 a22.4171,22.4171 0 1 0 0,31.7026 h168.2974 a22.4171,22.4171 0 1 0 0,-31.7026 Z");
Image hatch = createCrossHatch(lg);
ImagePattern pattern = new ImagePattern(hatch, 0, 0, 10, 10, false);
svgPath.setFill(pattern);
svgPath.setStroke(lg);
BorderPane root = new BorderPane();
root.setCenter(svgPath);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
catch (Exception e) {
e.printStackTrace();
}
}
protected static Image createCrossHatch(Paint paint) {
Pane pane = new Pane();
pane.setPrefSize(20, 20);
pane.setStyle("-fx-background-color: transparent;");
Line fw = new Line(-5, -5, 25, 25);
Line bw = new Line(-5, 25, 25, -5);
fw.setStroke(paint);
bw.setStroke(paint);
fw.setStrokeWidth(3);
bw.setStrokeWidth(3);
pane.getChildren().addAll(fw, bw);
new Scene(pane);
SnapshotParameters sp = new SnapshotParameters();
sp.setFill(Color.TRANSPARENT);
return pane.snapshot(sp, null);
}
private double getOffset(double i, int count) {
return (((double) 1) / (double) count * (double) i);
}
public static void main(String[] args) {
launch(args);
}
}
If you run the supplied code you will see it draws a dog bone. The lineargradient colors of the dashed outline should continue through the cross hatch ImagePattern fill. I'm aware of why the hatched ImagePattern is colored like it is but this is the best compromise I have at present. As mentioned I'd like to be able to applied the LinearGradient fill to the whole shape once the ImagePattern fill has been applied so the LinearGradient affects the whole shape the same.
Thanks
There is no direct way to apply and combine two paints over one single node. We can overlay many different paints (like solid, linear gradients or even image patterns) using background color via css, but that won't combine.
So in order to combine two different paints, on one side a linear gradient, on the other a pattern fill, we need to apply them to two nodes, and use a blending effect between both paints.
According to the code posted, this is the SVGPath with the linear gradient:
#Override
public void start(Stage primaryStage) {
Node base = getNodeWithGradient();
BorderPane root = new BorderPane();
Group group = new Group(base);
root.setCenter(group);
Scene scene = new Scene(root, 400, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
private SVGPath getNodeWithGradient() {
List<Color> colors = Arrays.asList(Color.RED, Color.YELLOW, Color.GREEN);
ArrayList<Stop> stops = new ArrayList<>(colors.size() * 2);
for (int i = 0; i < colors.size(); i++) {
stops.add(new Stop(getOffset(i, colors.size()), colors.get(i)));
stops.add(new Stop(getOffset(i + 1, colors.size()), colors.get(i)));
}
LinearGradient lg = new LinearGradient(0, 0, 20, 20, false, CycleMethod.REPEAT, stops);
SVGPath svgPath = getSVGPath();
svgPath.setFill(lg);
svgPath.setStroke(lg);
return svgPath;
}
private SVGPath getSVGPath() {
SVGPath svgPath = new SVGPath();
svgPath.setContent("M-84.1487,-15.8513 a22.4171,22.4171 0 1 0 0,31.7026 h168.2974 a22.4171,22.4171 0 1 0 0,-31.7026 Z");
return svgPath;
}
private double getOffset(double i, int count) {
return (((double) 1) / (double) count * (double) i);
}
While this is the SVGPath with the image pattern fill:
#Override
public void start(Stage primaryStage) {
Node overlay = getNodeWithPattern();
BorderPane root = new BorderPane();
Group group = new Group(overlay);
root.setCenter(group);
Scene scene = new Scene(root, 400, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
private SVGPath getNodeWithPattern() {
Image hatch = createCrossHatch();
ImagePattern pattern = new ImagePattern(hatch, 0, 0, 10, 10, false);
SVGPath svgPath = getSVGPath();
svgPath.setFill(pattern);
return svgPath;
}
private SVGPath getSVGPath() {
SVGPath svgPath = new SVGPath();
svgPath.setContent("M-84.1487,-15.8513 a22.4171,22.4171 0 1 0 0,31.7026 h168.2974 a22.4171,22.4171 0 1 0 0,-31.7026 Z");
return svgPath;
}
private static Image createCrossHatch() {
Pane pane = new Pane();
pane.setPrefSize(20, 20);
Line fw = new Line(-5, -5, 25, 25);
Line bw = new Line(-5, 25, 25, -5);
fw.setStroke(Color.BLACK);
bw.setStroke(Color.BLACK);
fw.setStrokeWidth(3);
bw.setStrokeWidth(3);
pane.getChildren().addAll(fw, bw);
new Scene(pane);
SnapshotParameters sp = new SnapshotParameters();
return pane.snapshot(sp, null);
}
Now the trick is to combine both SVGPath nodes, adding a blending mode to the one on top.
According to JavaDoc for BlendMode.ADD:
The color and alpha components from the top input are added to those from the bottom input.
#Override
public void start(Stage primaryStage) {
Node base = getNodeWithGradient();
Node overlay = getNodeWithPattern();
overlay.setBlendMode(BlendMode.ADD);
BorderPane root = new BorderPane();
Group group = new Group(base, overlay);
root.setCenter(group);
Scene scene = new Scene(root, 400, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
private SVGPath getNodeWithGradient() {
List<Color> colors = Arrays.asList(Color.RED, Color.YELLOW, Color.GREEN);
ArrayList<Stop> stops = new ArrayList<>(colors.size() * 2);
for (int i = 0; i < colors.size(); i++) {
stops.add(new Stop(getOffset(i, colors.size()), colors.get(i)));
stops.add(new Stop(getOffset(i + 1, colors.size()), colors.get(i)));
}
LinearGradient lg = new LinearGradient(0, 0, 20, 20, false, CycleMethod.REPEAT, stops);
SVGPath svgPath = getSVGPath();
svgPath.setFill(lg);
svgPath.setStroke(lg);
return svgPath;
}
private SVGPath getNodeWithPattern() {
Image hatch = createCrossHatch();
ImagePattern pattern = new ImagePattern(hatch, 0, 0, 10, 10, false);
SVGPath svgPath = getSVGPath();
svgPath.setFill(pattern);
return svgPath;
}
private SVGPath getSVGPath() {
SVGPath svgPath = new SVGPath();
svgPath.setContent("M-84.1487,-15.8513 a22.4171,22.4171 0 1 0 0,31.7026 h168.2974 a22.4171,22.4171 0 1 0 0,-31.7026 Z");
return svgPath;
}
private static Image createCrossHatch() {
Pane pane = new Pane();
pane.setPrefSize(20, 20);
Line fw = new Line(-5, -5, 25, 25);
Line bw = new Line(-5, 25, 25, -5);
fw.setStroke(Color.BLACK);
bw.setStroke(Color.BLACK);
fw.setStrokeWidth(3);
bw.setStrokeWidth(3);
pane.getChildren().addAll(fw, bw);
new Scene(pane);
SnapshotParameters sp = new SnapshotParameters();
return pane.snapshot(sp, null);
}
private double getOffset(double i, int count) {
return (((double) 1) / (double) count * (double) i);
}
And we get the desired result:
the inner shadow disappears during the transition it looks like the inner shadow is also scaled.
public void showTowerRange(int x0, int y0, double range) {
Circle circle = new Circle(
x0 * MAP_CELLS_WIDTH + MAP_CELLS_WIDTH / 2,
y0 * MAP_CELLS_HEIGHT + MAP_CELLS_HEIGHT / 2,
1,
Color.RED
);
circle.setOpacity(0.5);
gameRoot.getChildren().add(circle);
ScaleTransition scl = new ScaleTransition(Duration.millis(SHOW_RANGE_EFFECT_DURATION),circle);
scl.setByX(range * MAP_CELLS_WIDTH);
scl.setByY(range * MAP_CELLS_HEIGHT);
FadeTransition fd = new FadeTransition(Duration.millis(SHOW_RANGE_EFFECT_DURATION),circle);
fd.setByValue(-.3);
circle.setEffect(new InnerShadow(BlurType.GAUSSIAN, Color.GREEN, 4, 1, 0, 0));
circle.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.WHITE, 2, 0, 0, 0));
ParallelTransition prl = new ParallelTransition(scl,fd);
prl.play();
}
Well, I have good news and bad news for you. The good one - scale transition does not ruin inner shadow. The bad one - it is you ruining inner shadow:)
The point is, setEffect() method does not add another effect, it just set effect property of the Node, replacing previous value. What you need to do is to chain you effects (see example here). So instead of this:
circle.setEffect(new InnerShadow(BlurType.GAUSSIAN, Color.GREEN, 4, 1, 0, 0));
circle.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.WHITE, 2, 0, 0, 0));
You must do this:
InnerShadow is = new InnerShadow(BlurType.GAUSSIAN, Color.GREEN, 4, 1, 0, 0);
DropShadow ds = new DropShadow(BlurType.GAUSSIAN, Color.WHITE, 2, 0, 0, 0);
ds.setInput(is);
circle.setEffect(ds);
I have a gradient, e. g. green to red which ranges from 0 to 100. I need to lookup a color out of that gradient for any given value. Currently I'm painting a line on a canvas, fill it, take a snapshot and use a pixelreader to get the color. Does anyone know a better way? It seems like overkill to me.
A simple version of the code:
private Color getColor( double value) {
Canvas canvas = new Canvas(100, 1);
GraphicsContext gc = canvas.getGraphicsContext2D();
Stop[] stops = new Stop[] { new Stop(0, Color.GREEN), new Stop(1, Color.RED)};
LinearGradient linearGradient = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
gc.setFill(linearGradient);
gc.rect( 0, 0, canvas.getWidth(), canvas.getHeight());
gc.fill();
WritableImage image = new WritableImage((int) canvas.getWidth(), (int) canvas.getHeight());
image = canvas.snapshot(null, image);
PixelReader imageReader = image.getPixelReader();
Color imageColor = imageReader.getColor( (int) value, 0);
}
Thank you very much!
You can interpolate the color yourself:
Color imageColor = Color.GREEN.interpolate(Color.RED, value / 100.0);
I am making a math game and it involves making a graph. On some computers it works perfect but on some the graph does not match the picture box. It doesn't have to do with screen resolution as I have tried changing that with no difference. Any ideas on why it does not draw the same on every computer?
This is the code that draws the grid.
private void PaintGrid()
{
for (int i=1;i<20;i++) {
// Draw grid rectangle into the buffer
using (Graphics bufferGrph = Graphics.FromImage(buffer))
{
bufferGrph.DrawLine(new Pen(Color.Gray, 2), i * 30, 1, i * 30, 600);
bufferGrph.DrawLine(new Pen(Color.Gray, 2), 1, i * 30, 600,i * 30);
}
}
using (Graphics bufferGrph = Graphics.FromImage(buffer))
{
bufferGrph.DrawLine(new Pen(Color.CadetBlue, 5), 300, 1, 300, 600);
bufferGrph.DrawLine(new Pen(Color.CadetBlue, 5), 1, 300, 600, 300);
}
// Invalidate the panel. This will lead to a call of 'panel1_Paint'
panel1.Invalidate();
}