I have a ChartView item declared in QML and I need a rubberband-like zoom functionality. This can be achieved with semi-transparent rectangle and MouseArea item. The problem is with one rectangle it's only possible to select area from top-left to bottom-right due to the fact that Rectangle item with negative dim-s is either invisible or disabled. Although it's possible to apply transform to Rectangle
transform: Scale { origin.x: 0; origin.y: 0; xScale: -1}
I failed to find how to manipulate xScale/yScale properties from the outside.
Right now I draw 4 rectangles, one per each quadrant, with correct xScale/yScale and dims (code in the end).
So I wonder is there more elegant/easy solution to the problem?
ChartView {
id: chartViewTop
...
Rectangle{
id: rubberBandRec1
border.color: "black"
border.width: 1
opacity: 0.3
visible: false
transform: Scale { origin.x: 0; origin.y: 0; yScale: -1}
}
Rectangle{
id: rubberBandRec2
border.color: "black"
border.width: 1
opacity: 0.3
visible: false
transform: Scale { origin.x: 0; origin.y: 0; yScale: -1; xScale: -1}
}
Rectangle{
id: rubberBandRec3
border.color: "black"
border.width: 1
opacity: 0.3
visible: false
transform: Scale { origin.x: 0; origin.y: 0; xScale: -1}
}
Rectangle{
id: rubberBandRec4
border.color: "black"
border.width: 1
opacity: 0.3
visible: false
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onPressed: {
rubberBandRec1.x = mouseX; rubberBandRec1.y = mouseY; rubberBandRec1.visible = true;
rubberBandRec2.x = mouseX; rubberBandRec2.y = mouseY; rubberBandRec2.visible = true;
rubberBandRec3.x = mouseX; rubberBandRec3.y = mouseY; rubberBandRec3.visible = true;
rubberBandRec4.x = mouseX; rubberBandRec4.y = mouseY; rubberBandRec4.visible = true;
}
onMouseXChanged: {
rubberBandRec1.width = mouseX - rubberBandRec1.x;
rubberBandRec2.width = rubberBandRec2.x-mouseX;
rubberBandRec3.width = rubberBandRec3.x-mouseX;
rubberBandRec4.width = mouseX - rubberBandRec4.x;
}
onMouseYChanged: {
rubberBandRec1.height = rubberBandRec1.y - mouseY;
rubberBandRec2.height = rubberBandRec2.y - mouseY;
rubberBandRec3.height = mouseY - rubberBandRec3.y;
rubberBandRec4.height = mouseY - rubberBandRec4.y;
}
onReleased: {
var x = rubberBandRec4.x-(rubberBandRec4.width<0)*Math.abs(rubberBandRec4.width);
var y = rubberBandRec4.y-(rubberBandRec4.height<0)*Math.abs(rubberBandRec4.height);
if (Math.abs(rubberBandRec4.width*rubberBandRec4.height)>100)
chartViewTop.zoomIn(Qt.rect(x, y, Math.abs(rubberBandRec4.width),
Math.abs(rubberBandRec4.height)));
rubberBandRec1.visible = false;
rubberBandRec2.visible = false;
rubberBandRec3.visible = false;
rubberBandRec4.visible = false;
}
}
}
Set external properties for the scaling, and then just change these in the onMouseXChanged and onMouseYChanged events as follows. This appears to be working for me:
property int xScaleZoom: 0
property int yScaleZoom: 0
Rectangle{
id: recZoom
border.color: "steelblue"
border.width: 1
color: "steelblue"
opacity: 0.3
visible: false
transform: Scale { origin.x: 0; origin.y: 0; xScale: xScaleZoom; yScale: yScaleZoom}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onPressed: {
recZoom.x = mouseX;
recZoom.y = mouseY;
recZoom.visible = true;
}
onMouseXChanged: {
if (mouseX - recZoom.x >= 0) {
xScaleZoom = 1;
recZoom.width = mouseX - recZoom.x;
} else {
xScaleZoom = -1;
recZoom.width = recZoom.x - mouseX;
}
}
onMouseYChanged: {
if (mouseY - recZoom.y >= 0) {
yScaleZoom = 1;
recZoom.height = mouseY - recZoom.y;
} else {
yScaleZoom = -1;
recZoom.height = recZoom.y - mouseY;
}
}
onReleased: {
var x = (mouseX >= recZoom.x) ? recZoom.x : mouseX
var y = (mouseY >= recZoom.y) ? recZoom.y : mouseY
chartView.zoomIn(Qt.rect(x, y, recZoom.width, recZoom.height));
recZoom.visible = false;
}
}
I can create Nodes on a pane with a for next loop, but do not have the ability
to assign these nodes a fx:id or a id
Is this possible number one? If so what do I need to add to my code?
Or do I have the option to write the information to a FXML file with the for next loop?
private void MakeNode(){
for (int A = 1; A <= 42; A++){
if(A==1){
X=40;
Y=40;
}else if(A>1 && A<=7){
X = X + 120;
Y = 40;
}else if(A==8){
X = 40;
Y = 160;
}else if(A>8&& A<=14){
X = X + 120;
Y = 160;
}else if(A==15){
X = 40;
Y = 280;
}else if(A>15&& A<=21){
X = X + 120;
Y = 280;
}else if(A==22){
X = 40;
Y = 400;
}else if(A>22&& A<=28){
X = X + 120;
Y = 400;
}else if(A==29){
X = 40;
Y = 520;
}else if(A>29&& A<=35){
X = X + 120;
Y = 520;
}else if(A==36){
X = 40;
Y = 640;
}else if(A>36&& A<=42){
X = X + 120;
Y = 640;
}
cirA = new Circle(X,Y,16);
//fxid = cir.concat(String.valueOf(A));
//fxid = cir+String.valueOf(A);
//cirA.setId(fxid);
cirA.setFill(Color.YELLOW);
cirA.setStroke(Color.BLACK);
cirA.setStrokeWidth(4.0);
pane.getChildren().add(cirA);
}
}
fx:id is just a mechanism for getting a reference to elements defined in FXML to the controller. If you are defining nodes in the controller anyway, there is no need for (or indeed, no way to use) fx:id at all.
You already have a reference to the Circle when you create it. So just do whatever you need right there. Here's a simple example (I cleaned your code up to make it much less verbose):
private void makeNode() {
for (int circleIndex = 0 ; circleIndex < 42 ; circleIndex++) {
int column = circleIndex % 7 ;
int row = circleIndex / 7 ;
double x = 40 + 120 * column ;
double y = 40 + 120 * row ;
Circle circle = new Circle(x, y, 16);
circle.setFill(Color.YELLOW);
circle.setStroke(Color.BLACK);
circle.setStrokeWidth(4.0);
pane.getChildren().add(circle);
circle.setOnMouseClicked(e -> {
System.out.println("Clicked on ["+column+", "+row+"]");
});
}
}
I have a written a simple program. I am able to see two minor spokes between the angles I am drawing, and I am not sure why.
import QtQuick 2.0
Rectangle {
width: 360
height: 360
Canvas
{
width: 360
height: 360
onPaint:
{
var x = 140
var y = 140
var radius = 140;
var startangle = -90
var endangle = 30
var context = getContext("2d");
for(var j=0; j < 3; j++)
{
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, radius, (startangle)*(Math.PI/180), (endangle)*(Math.PI/180), false) //x, y, radius, startAngle, endAngle, anticlockwise
context.fillStyle = "blue"
context.fill();
startangle += 120
endangle += 120
context.closePath();
}
}
}
}
output:
The expected drawing should not have spokes (stroke lines).
I have the following test code where I have a ClothMesh (from FXyz lib) that I
can drag, rotate and drag my circle handles. All works well, FXyz is great. Now I want to use SegmentedSphereMesh, it mostly work except that my circle handles are 2D and not wrapped around the sphere. I know the possible problems mixing 2D and 3D. However, it is so close to working; how can I make my handles work with the sphere, or what would be another way to do the same function.
Note, I do not want to control the shape/mesh by moving the camera around.
import org.fxyz.shapes.complex.cloth.ClothMesh
import org.fxyz.shapes.primitives.SegmentedSphereMesh
import scalafx.Includes._
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.beans.property.DoubleProperty
import scalafx.collections.ObservableFloatArray
import scalafx.scene.image.Image
import scalafx.scene.input.{MouseButton, MouseEvent}
import scalafx.scene.paint.PhongMaterial
import scalafx.scene.shape._
import scalafx.scene.transform.Rotate
import scalafx.scene._
import scalafx.scene.paint.Color
/**
* left mouse to drag the meshView and also to drag the handles
* right mouse drag + ctrl to rotate about X axis
* right mouse drag + alt to rotate about Y axis
* right mouse drag + shift to rotate about Z axis
*/
object ClothTest2 extends JFXApp {
private var dx = 0.0
private var dy = 0.0
stage = new PrimaryStage {
scene = new Scene(600, 600, true, SceneAntialiasing.Balanced) {
fill = Color.LightGray
val testImg = "https://upload.wikimedia.org/wikipedia/commons/c/c4/PM5544_with_non-PAL_signals.png"
val img = new Image(testImg, 400, 400, false, true)
val meshView = new SegmentedSphereMesh(20, 4, 2, 200d)
// val meshView = new ClothMesh(4, 4, 200, 200, 0.5, 0.5, 1.0)
meshView.setDrawMode(DrawMode.Fill)
meshView.setCullFace(CullFace.None)
meshView.style = "-fx-background-color: #00000000"
meshView.setMaterial(new PhongMaterial(Color.White, img, null, null, null))
val controller = new MeshController(meshView.getMesh().asInstanceOf[javafx.scene.shape.TriangleMesh].points)
val viewGroup = new Group(meshView, controller)
root = new Group(new AmbientLight(Color.White), viewGroup) { translateX = 70; translateY = 70 }
camera = new PerspectiveCamera(true) {
nearClip = 0.0
farClip = 100000.0
fieldOfView = 42
verticalFieldOfView = true
translateZ = -900
}
val rotHandler = new RotHandler(viewGroup)
onMouseDragged = (event: MouseEvent) => {
rotHandler.onMouseDragged(event)
if (event.button == MouseButton.PRIMARY) {
viewGroup.layoutX = event.sceneX + dx
viewGroup.layoutY = event.sceneY + dy
event.consume()
}
}
onMousePressed = (event: MouseEvent) => {
rotHandler.onMousePressed(event)
dx = viewGroup.layoutX.value - event.sceneX
dy = viewGroup.layoutY.value - event.sceneY
event.consume()
}
}
}
}
class CircleHandle(color: Color) extends Circle {
radius = 8
var dx = 0.0
var dy = 0.0
fill <== when(hover) choose Color.Red.deriveColor(1, 1, 1, 0.4) otherwise color.deriveColor(1, 1, 1, 0.4)
strokeWidth <== when(hover) choose 3 otherwise 2
stroke = color
onMousePressed = (event: MouseEvent) => {
dx = centerX.value - event.x
dy = centerY.value - event.y
event.consume()
}
onMouseDragged = (event: MouseEvent) => {
centerX = event.x + dx
centerY = event.y + dy
event.consume()
}
}
class MeshController(thePoints: ObservableFloatArray) extends Group {
children = for (i <- 0 until thePoints.size by 3) yield new CircleHandle(Color.Yellow) {
centerX() = thePoints.get(i)
centerX.onChange { (obs, oldVal, newVal) => thePoints.set(i, newVal.floatValue()) }
centerY() = thePoints.get(i + 1)
centerY.onChange { (obs, oldVal, newVal) => thePoints.set(i + 1, newVal.floatValue()) }
}
}
class RotHandler(val viewer: Group) {
private val angleX = DoubleProperty(0)
private val angleY = DoubleProperty(0)
private val angleZ = DoubleProperty(0)
private var anchorX = 0d
private var anchorY = 0d
private val rotX = new Rotate { angle <== angleX; axis = Rotate.XAxis }
private val rotY = new Rotate { angle <== angleY; axis = Rotate.YAxis }
private val rotZ = new Rotate { angle <== angleZ; axis = Rotate.ZAxis }
viewer.transforms = Seq(rotX, rotY, rotZ)
def onMousePressed(event: MouseEvent) = {
anchorX = event.sceneX
anchorY = event.sceneY
event.consume()
}
def onMouseDragged(event: MouseEvent) = {
// right mouse only
if (event.button == MouseButton.SECONDARY) {
event match {
// rotation about the Y axis, dragging the mouse in the x direction
case ev if ev.altDown => angleY() = anchorX - event.sceneX
// rotation about the X axis, dragging the mouse in the y direction
case ev if ev.controlDown => angleX() = anchorY - event.sceneY
// rotation about the Z axis, dragging the mouse in the x direction
case ev if ev.shiftDown => angleZ() = anchorX - event.sceneX
case _ => // ignore everything else
}
}
event.consume()
}
}
I have this part of code
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'getNewUsers.php',false);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send();
var json;
if (xhr.readyState == 4 && xhr.status == 200) { // If file is loaded correctly.
json = JSON.parse(xhr.responseText);
alert(json.en);
}
else if(xhr.readyState == 4 && xhr.status != 200) { // En cas d'erreur !
alert( ' Une erreur est survenue !\n\nCode :' + xhr.status + '\nTexte : ' + xhr.statusText);
}
</script>
<script id="bs">
function Sector(x, y, radius, startAngle, endAngle) {
SpecialAttrPath.call(this, {
radius: 0,
startAngle: startAngle,
endAngle: endAngle
});
this.attr({
x: x,
y: y,
radius: radius,
startAngle: startAngle,
endAngle: endAngle
});
}
Sector.prototype = Object.create(SpecialAttrPath.prototype);
Sector.prototype._make = function() {
var attr = this._attributes,
radius = attr.radius,
startAngle = attr.startAngle,
endAngle = attr.endAngle;
var startX, startY, endX, endY;
var diffAngle = Math.abs(endAngle - startAngle);
this.startX = startX = radius * Math.cos(startAngle);
this.startY = startY = radius * Math.sin(startAngle);
if (diffAngle < Math.PI*2) {
endX = radius * Math.cos(endAngle);
endY = radius * Math.sin(endAngle);
} else { // angles differ by more than 2*PI: draw a full circle
endX = startX;
endY = startY - .0001;
}
this.endX = endX;
this.endY = endY;
this.radiusExtentX = radius * Math.cos(startAngle + (endAngle - startAngle)/2);
this.radiusExtentY = radius * Math.sin(startAngle + (endAngle - startAngle)/2);
return this.moveTo(0, 0)
.lineTo(startX, startY)
.arcTo(radius, radius, 0, (diffAngle < Math.PI) ? 0 : 1, 1, endX, endY)
.lineTo(0, 0);
};
Sector.prototype.getDimensions = function() {
var x = this.attr('x'),
y = this.attr('y'),
left = Math.min(x, x + this.startX, x + this.endX, x + this.radiusExtentX),
top = Math.min(y, y + this.startY, y + this.endY, y + this.radiusExtentY),
right = Math.max(x, x + this.startX, x + this.endX, x + this.radiusExtentX),
bottom = Math.max(y, y + this.startY, y + this.endY, y + this.radiusExtentY);
console.log(y, y + this.startY, y + this.endY, y + this.radiusExtentY)
return {
left: left,
top: top,
width: right - left,
height: bottom - top
};
};
PieChart.BASE_COLOR = color('red');
function PieChart(data) {
this.angle = 0;
this.labelY = 30;
this.kolor = PieChart.BASE_COLOR.clone();
var n = 0;
for (var i in data) {
this.slice(i, data[i], n++);
}
}
PieChart.prototype = {
slice: function(name, value, i) {
var start = this.angle,
end = start + (Math.PI*2) * value/100,
// Increase hue by .1 with each slice (max of 10 will work)
kolor = this.kolor = this.kolor.clone().hue(this.kolor.hue()+.1);
var s = new Sector(
400, 200, 150,
start,
end
);
var animDelay = (i * 200) + 'ms';
var label = this.label(name, value, kolor);
label.attr({ opacity: 0 });
s.stroke('#FFF', 3);
s.fill(kolor);
s.attr({
endAngle: start,
radius: 0
}).addTo(stage).on('mouseover', over).on('mouseout', out);
label.on('mouseover', over).on('mouseout', out);
function over() {
label.text.attr('fontWeight', 'bold');
label.animate('.2s', {
x: 40
});
s.animate('.2s', {
radius: 170,
fillColor: kolor.lighter(.1)
}, {
easing: 'sineOut'
});
}
function out() {
label.text.attr('fontWeight', '');
label.animate('.2s', {
x: 30
});
s.animate('.2s', {
radius: 150,
fillColor: kolor
});
}
s.animate('.4s', {
radius: 150,
startAngle: start,
endAngle: end
}, {
easing: 'sineOut',
delay: animDelay
});
label.animate('.4s', {
opacity: 1
}, { delay: animDelay });
this.angle = end;
},
label: function(name, v, fill) {
var g = new Group().attr({
x: 30,
y: this.labelY,
cursor: 'pointer'
});
var t = new Text(name + ' (' + v + '%)').addTo(g);
var r = new Rect(0, 0, 20, 20, 5).fill(fill).addTo(g);
t.attr({
x: 30,
y: 17,
textFillColor: 'black',
fontFamily: 'Arial',
fontSize: '14'
});
g.addTo(stage);
this.labelY += 30;
g.text = t;
return g;
}
};
new PieChart({
English: json.en,
French: 20,
German: 30,
Dutch: 5,
Spanish: 19,
Others: 18
})
</script>
The problem is I would like to change the pie dynamically using Json, in the demi it is shown with integers but here it is also an integer. This is the line that cause the problem for rendering the pie.
new PieChart({
English: json.en,
BonsaiJS is executed in a separate execution environment (mostly worker) and because of that it can't reach objects that were defined outside (in your case the json variable) of the BonsaiJS movie code (in your example <script id="bs">).
You can read about the special execution of BonsaiJS here: http://docs.bonsaijs.org/overview/Execution.html. If you want to pass data from your page to the Bonsai movie execution you can do the following:
Dynamically communicate through sendMessage with the execution context (described here: http://docs.bonsaijs.org/overview/Communication.html)
If you just have to pass data into your context once you can do that through bonsai.run(myNode, {myJson: json}); and access it from within your movie code through stage.options.myJson (documented on the bottom of http://docs.bonsaijs.org/overview/Execution.html).
You also have the third option to move the XMLHttpRequest code into the movie-code and do the request from there. Every client-side bonsai execution context (worker, iframe) does support that.