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
}
}
}

Nessun commento:

Posta un commento