Visualizzazione post con etichetta nb. Mostra tutti i post
Visualizzazione post con etichetta nb. Mostra tutti i post

PerspectiveTransform

Altra applicazione di esempio per JavaFX fornita da Netbeans é PhotoFlip, che mostra l'uso della classe PerspectiveTransform.

Mi vien da pensare che questa applicazione sia stata scritta da uno che ha una ottima conoscenza della classe in questione ma non sia così addentro nelle logiche di JavaFX, dato che il codice é un po' meno leggibile di quanto si veda normalmente.

Inoltre a me, che poco ne capisco di grafica, l'uso della classe m'é risultato oscuro. Quindi sono costretto a fare atto di fede su questo punto, che sarebbe dovuto essere fondamentale, e mi dedico piuttosto a capire il funzionamento dell'applicazione al contorno.

Useremo due file, FlipMain.fx e FlipView.fx, contentente codice proveniente dall'originale Main.fx e riarrangiato per rendermelo più adatto al mio gusto (fra l'altro, ho rimosso il codice per costruire la simulazione di un bottone per usare un vero bottone - si rimanda al codice originale se si preferisce una vita più complicata del necessario).
Le foto sono state mosse in una sottodirectory del package corrente, a nome image, per mantenere le convenzioni utilizzate solitamente negli altri tutorial.

Stage e scena

Iniziamo creando la scena su cui avverrà la nostra azione, dovrebbe essere ormai una cosa che ci viene quasi naturale:

package fliptransition;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

Stage {
title: "Flip Transition"
resizable: false

scene: Scene {
fill: Color.BLACK
width: 300, height: 340
}
}

Unico punto degno di nota é che si é specificato che non vogliamo che la finestra possa cambiar dimensione, si tratta della proprietà resizable dello stage.

FlipView e bottone

Vogliamo mettere in scena due oggetti: un bottone e un nodo custom che ci creiamo noi, FlipView che ci permetterà di vedere due immagine, alternandole con un simpatico movimento di rotazione.

Diamo quindi una prima implementazione di FlipView, nel file FlipView.fx:

package fliptransition;

import javafx.scene.CustomNode;
import javafx.scene.Node;

public class FlipView extends CustomNode {
public-init var frontNode : Node;
public-init var backNode : Node;

override public function create() : Node {
frontNode
}

public function flip() : Void {
}
}

La nostra classe FlipView specializza un CustomNode ed é visibile a chiunque abbia accesso al package.
Al suo interno ci sono due nodi che possono essere inizializzati ma mai più modificati (le immagini che vogliamo visualizzare) e un boolean che può essere solo letto e ci dice se stiamo vedendo la foto di fronte o quella del retro.
Abbiamo poi definito la funzione create() che, al momento, torna l'immagine frontale e la funzione flip() che implementerà il cambio di foto visualizzata.

Torneremo più avanti su questa classe, al momento questo ci basta per andare avanti nello sviluppo del nostro script fx nel main.

Definiamo infatti un oggetto di tipo FlipView e piazziamolo in basso al centro della nostra scena. Le immagini che mettiamo in FlipView sono quelle fornite dall'esempio (ma spostate nella sottodirectory image):

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

def flipView = FlipView {
translateX: 50, translateY: 90
backNode: ImageView { image: Image { url: "{__DIR__}image/lion1.png" } }
frontNode: ImageView { image: Image { url: "{__DIR__}image/lion2.png" } }
};


L'oggetto di tipo bottone sarà invece definito così:

import javafx.scene.control.Button;
import javafx.scene.text.Font;

def button = Button {
translateX: 50, translateY: 10
width: 200, height: 35
text: "Click Here to Flip", font: Font { size: 18 }
action: flipView.flip;
}

Nota che abbiamo definito che l'azione associata al bottone é la funzione flip() del nostro oggetto flipView. Quindi cliccare sul bottone vuol dire richiamare quella funzionalità.

Aggiungiamo il bottone e la vista alla scena:

Stage {
...
scene: Scene {
...
content: [ flipView, button ]
}
}

E abbiamo già una prima versione della nostra applicazione. Mancano la funzionalità per cambiare le immagini ma il nostro script nel main é oramai completo. Ecco a seguire il suo codice:

package fliptransition;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.control.Button;
import javafx.scene.text.Font;

def flipView = FlipView {
translateX: 50, translateY: 90
backNode: ImageView { image: Image { url: "{__DIR__}image/lion1.png" } }
frontNode: ImageView { image: Image { url: "{__DIR__}image/lion2.png" } }
};

def button = Button {
translateX: 50, translateY: 10
width: 200, height: 35
text: "Click Here to Flip", font: Font { size: 18 }
action: flipView.flip;
}

Stage {
title: "Flip Transition"
resizable: false

scene: Scene {
fill: Color.BLACK
width: 300, height: 340
content: [ flipView, button ]
}
}

Passiamo quindi a considerare la classe FlipView. Iniziamo dalla funzione cardine, quella che calcola la trasformazione da applicare alla nostra immagine mentre passiamo da una all'altra foto. In pratica si tratta di costruire un oggetto PerspectiveTransform in funzione del tempo da quando é iniziata la trasformazione. Non dico di più su come sia fatta perché a me la cosa é piuttosto oscura:

import javafx.scene.effect.PerspectiveTransform;
import javafx.util.Math;
...
function getPT(t : Number) : PerspectiveTransform {
def width = 200;
def height = 200;
def radius = width/2;
def back = height/10;

return PerspectiveTransform {
ulx: radius - Math.sin(t)*radius, uly: 0 - Math.cos(t)*back
urx: radius + Math.sin(t)*radius, ury: 0 + Math.cos(t)*back
lrx: radius + Math.sin(t)*radius, lry: height - Math.cos(t)*back
llx: radius - Math.sin(t)*radius, lly: height + Math.cos(t)*back
}
}

Dichiariamo ora una variabile, where, che tiene traccia della rotazione che applichiamo alla nostra immagine. La posizione iniziale la chiamiamo pi greco mezzi, e rotiamo fino ad arrivare a meno pi greco mezzi. Quando siamo a zero, l'immagine é perpendicolare allo schermo. Quando l'angolo é positivo si vedrà l'immagine frontale, quando l'angolo é negativo si vedrà l'immagine sul retro.

Per gestire questo algoritmo usiamo un Timeline, che ci farà variare where linearmente in un secondo:

import javafx.animation.Interpolator;
import javafx.animation.Timeline;

...

var where = Math.PI/2;
var anim = Timeline {
keyFrames: [
at(0s) { where => Math.PI/2}
at(1s) { where => -Math.PI/2 tween Interpolator.LINEAR}
]
}

Fatto questo, possiamo scrivere l'implementazione della funzione flip(), che viene richiamata pigiando il bottone per cambiare immagine:

public function flip() : Void {
if(where < 0) {
anim.rate = -1.0;
anim.time = 1s;
}
else {
anim.rate = 1.0;
anim.time = 0s;
}
anim.play();
}

Se l'angolo é negativo, dobbiamo fare un movimento inverso, quindi facciamo partire l'animazione in senso contrario, altrimenti la facciamo partire nel verso giusto.

Ultima cosa, dobbiamo completare la funzione create(), per far sì che siano disponibili le due immagini che saranno distorte a seconda del valore di where (e quindi a seconda del tempo trascorso dall'inizio dello spostamento), usando l'algoritmo definito dalla getPT().

import javafx.scene.Group;
...
override public function create() : Node {
return Group {
content: [
Group {
content: backNode
visible: bind (where < 0)
effect: bind getPT(where)
},
Group {
content: frontNode
visible: bind (where > 0)
effect: bind getPT(where)
}
]
}
}

Questo completa il codice della classe FlipView, che vediamo ora al completo:

package fliptransition;

import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.effect.PerspectiveTransform;
import javafx.util.Math;

protected class View extends CustomNode {
public-init var frontNode : Node;
public-init var backNode : Node;

var where = Math.PI/2;
var anim = Timeline {
keyFrames: [
at(0s) { where => Math.PI/2}
at(1s) { where => -Math.PI/2 tween Interpolator.LINEAR}
]
}

public function flip() : Void {
if(where < 0) {
anim.rate = -1.0;
anim.time = 1s;
}
else {
anim.rate = 1.0;
anim.time = 0s;
}
anim.play();
}

override public function create() : Node {
return Group {
content: [
Group {
content: backNode
visible: bind (where < 0)
effect: bind getPT(where)
},
Group {
content: frontNode
visible: bind (where > 0)
effect: bind getPT(where)
}
]
}
}

function getPT(t : Number) : PerspectiveTransform {
def width = 200;
def height = 200;
def radius = width/2;
def back = height/10;

return PerspectiveTransform {
ulx: radius - Math.sin(t)*radius, uly: 0 - Math.cos(t)*back
urx: radius + Math.sin(t)*radius, ury: 0 + Math.cos(t)*back
lrx: radius + Math.sin(t)*radius, lry: height - Math.cos(t)*back
llx: radius - Math.sin(t)*radius, lly: height + Math.cos(t)*back
}
}
}

Drag and drop

Netbeans fornisce una serie di applicazioni di esempio per JavaFX. La prima che vediamo é sull'uso del drag and drop.

Delle due versioni presentate, analizziamo la seconda, basata sullo script Main2.fx che richiama la classe DragBehavior definita nell'omonimo file.

E' stato fatto qualche cambiamento al codice, per renderlo più adeguato al gusto di chi scrive, ma sostanzialmente ci si può ritrovare anche guardando il codice originale.

Si tratta di visualizzare una palla, che potrà essere trascinata su tutta la scena, ma senza che esca dai limiti, nemmeno parzialmente.

Cominciamo a definire lo stage e la scena della nostra applicazione in Main.fx, che alla fine risulterà molto simile a Main2.fx:

package draganddrop;

import javafx.scene.Scene;
import javafx.stage.Stage;

def scene : Scene = Scene {
width: 240, height: 320
}

Stage {
title: "Drag And Drop"
scene: scene
}

Coloriamo il fondale della scena con una sfumatura che va dal bianco sporco al marrone.

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
...
def scene : Scene = Scene {
...
fill: LinearGradient {
proportional: true, startX: 0, startY: 0, endX: 0, endY: 1

stops: [
Stop { offset: 0.0, color: Color.WHITESMOKE },
Stop { offset: 1.0, color: Color.BROWN },
]
}
}

Creiamo un immagine e mettiamola nella scena. l'immagine é fornita con l'esempio, noi ci aggiungiamo una ombreggiatura per darle un effetto tridimensionale. Nota che nell'esempio di Netbeans si usa l'altro .png disponibile, che ha un effetto incluso nell'immagine stessa, che però la rende di gestione più complessa per il problema del movimento sui bordi. L'uso di ball2.png permette di risovere la questione in modo decisamente più elegante.

import javafx.scene.effect.InnerShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
...
def ball = ImageView {
image: Image { url: "{__DIR__}images/ball2.png" }
effect: InnerShadow{offsetX: 8, offsetY: 8, color: Color.PINK}
};

def scene : Scene = Scene {
...
content: ball
...
}

Quando ridimensionsioniamo la finestra della applicazione vogliamo evitare che la palla resti fuori dalla nostra visuale. Dunque creiamo due variabili collegate alle dimensioni della scena al fine di associare un trigger che, in caso di restringimento, riporti la palla al bordo estremo.

var width : Number = bind scene.width on replace oldValue {
if(width < oldValue) {
ball.translateX = 0;
}
};

var height : Number = bind scene.height on replace oldValue {
if(height < oldValue) {
ball.translateY = 0;
}
};

La nostra scena é a posto, resta ora da definire la modalità di funzionamento del drag and drop sulla palla, che deleghiamo a una apposita classe, DragDrop - basata sulla DragBehavior fornita da Netbeans, che definiamo nel suo specifico file DragDrop.fx:

package draganddrop;

import javafx.scene.Node;

public class DragDrop {
public-init var target : Node;
public-init var targetWidth : Number;
public-init var targetHeight : Number;

public var maxX;
public var maxY;
}

Dichiariamo tre variabili che definiscono il bersaglio del drag and drop:
  • target, rappresenta l'oggetto come viene gestito all'interno della scena
  • targetWidth e targetHeight, che rappresentano le dimensioni del bersaglio
é stata data loro una visibilità public-init, dato che una volta inizializzate non le si vuole più cambiare.
Poi abbiamo altre due variabili, maxX e maxY, che rappresentano la dimensione della scena, e sono quindi il limite estremo che i nostro oggetto può raggiungere nella finestra verso destra e verso il basso. Dato che é possibile ridimensionare la finestra, il loro valore può cambiare nel corso dell'esecuzione del programma, e quindi abbiamo dato loro visibilità public.

Data questa definizione della classe DragDrop, possiamo completare il nostro script aggiungendo la sua instaziazione, che renderà disponibile all'applicazione le funzionalità di drag and drop.

DragDrop {
target: ball
targetWidth: ball.image.width
targetHeight: ball.image.height
maxX: bind scene.width
maxY: bind scene.height
};

Il obiettivo del drag and drop é la palla che abbiamo messo nella scena. La dimensione dell'obiettivo viene ricavata dall'ampiezza e altezza dell'immagine associata alla palla.
Leghiamo poi maxX e maxY all'ampiezza e altezza della scena, in modo che l'aggiornamento delle dimensioni della scena si rifletta automaticamente sulle variabili membro della classe DragDrop.

E quindi questo é il codice completo per main.fx:

package draganddrop;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.stage.Stage;

import javafx.scene.effect.InnerShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

var width : Number = bind scene.width on replace oldValue {
if(width < oldValue) {
ball.translateX = 0;
}
};

var height : Number = bind scene.height on replace oldValue {
if(height < oldValue) {
ball.translateY = 0;
}
};

def ball = ImageView {
image: Image { url: "{__DIR__}images/ball2.png" }
effect: InnerShadow{offsetX: 8, offsetY: 8, color: Color.PINK}
};

def scene : Scene = Scene {
width: 240
height: 320
content: ball

fill: LinearGradient {
proportional: true, startX: 0, startY: 0, endX: 0, endY: 1

stops: [
Stop { offset: 0.0, color: Color.WHITESMOKE },
Stop { offset: 1.0, color: Color.BROWN },
]
}
}

DragDrop {
target: ball
targetWidth: ball.image.width
targetHeight: ball.image.height
maxX: bind scene.width
maxY: bind scene.height
};

Stage {
title: "Drag And Drop"
scene: scene
}

Ci resta ora da implementare le funzionalità per il drag and drop. Per far questo definiamo un blocco init all'interno della DragDrop in cui dichiariamo per il nostro target il comportamento che deve tenere per due eventi del mouse, quando lo si preme - tramite la funzione onMousePressed(), e quando lo si trascina - funzione onMouseDragged():

import javafx.scene.input.MouseEvent;
...
init {
target.onMousePressed = function(e : MouseEvent) : Void {
println("click!");
}

target.onMouseDragged = function(e : MouseEvent) : Void {
println("moving");
}
}

Il nostro target (e solo lui) ora risponde ai click del mouse. Vediamo di fare in modo che risponda muovendo l'oggetto. Quando clicchiamo il mouse sull'oggetto vogliamo registrare la posizione in cui lo facciamo relativamente all'oggetto stesso. Per far questo dichiariamo due variabili startX e startY e assegnamo loro un valore nella onMousePressed() nel modo seguente:

var startX;
var startY;
...
target.onMousePressed = function(e : MouseEvent) : Void {
startX = e.sceneX - target.translateX;
startY = e.sceneY - target.translateY;
println("click [{startX}, {startY}]");
}

Definiamo startX come la ascissa del punto dove abbiamo cliccato meno l'ascissa del target (ovvero del suo punto in alto a sinistra), ottenendo in questo modo l'ascissa del click relativa all'oggetto. Lo stesso per startY rispetto alle ordinate. Dunque, se clicco in mezzo alla palla otterrò sempre un valore della coppia di 35, 35. Ovunque sia la palla nella scena.


Riscriviamo ora la onMouseDragged(), per muovere il nostro oggetto sulla scena mentre il mouse viene trascinato.

target.onMouseDragged = function(e : MouseEvent) : Void {
var tx = e.sceneX - startX;
var ty = e.sceneY - startY;

if(tx < 0) { tx = 0; }
else if(tx > maxX - targetWidth) { tx = maxX - targetWidth; }

if(ty < 0) { ty = 0; }
else if(ty > maxY - targetHeight) { ty = maxY - targetHeight; }

target.translateX = tx;
target.translateY = ty;
}

Per prima cosa ci calcoliamo le nuove coordinate dell'oggetto. La sua ascissa sarà pari all'ascissa del mouse sulla scena meno il valore dell'ascissa del mouse relativa all'oggetto. E facciamo lo stesso con l'ordinata.

Prima di spostare l'oggetto (cosa che si fa semplicemente cambiando i suoi valori translateX e translateY - ci pensa JavaFX ai dettagli) vogliamo fare in modo di restringere lo spostamento alla scena stessa.

Per cui, se l'ascissa del target che abbiamo calcolato risulta inferiore di zero, la mettiamo a zero (non é possibile andare più a sinistra del margine sinistro della finestra), se é più grande dell'ampiezza della finestra meno l'ampiezza dell'oggetto, allora le assegnamo questo valore limite (non é possibile che il nostro oggetto "trasbordi" a destra del margine della finestra). E facciamo lo stesso con le ordinate.

Il codice risultante per DragDrop.fx é questo:

package draganddrop;

import javafx.scene.Node;
import javafx.scene.input.MouseEvent;

public class DragDrop {
// the target won't change
public-init var target : Node;
public-init var targetWidth : Number;
public-init var targetHeight : Number;
// the scene size may vary
public var maxX;
public var maxY;

// for target dragging, relative cursor position in the target
var startX;
var startY;

init {
// register the mouse position within the target
target.onMousePressed = function(e : MouseEvent) : Void {
startX = e.sceneX - target.translateX;
startY = e.sceneY - target.translateY;
// println("click [{startX}, {startY}]");
}

// move the target, but not outside the scene!
target.onMouseDragged = function(e : MouseEvent) : Void {
var tx = e.sceneX - startX;
var ty = e.sceneY - startY;

if(tx < 0) { tx = 0;}
else if(tx > maxX - targetWidth) { tx = maxX - targetWidth; }

if(ty < 0) { ty = 0; }
else if(ty > maxY - targetHeight) { ty = maxY - targetHeight; }

target.translateX = tx;
target.translateY = ty;
}
}
}