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

SpringAnimation

Applicazione di JavaFX presa dagli esempi forniti con Netbeans 6.5 (noto per inciso che al momento la beta di Netbeans 6.7 non include il supporto per JavaFX e la cosa mi preoccupa un po', dato che da quando Sun é stata inglobata da Oracle girano strane voci sul futuro di questo linguaggio).

Lo scopo é quello di mostrare come estendere la classe SimpleInterpolator per ottenere animazioni custom.

Per far ciò si crea un applicazione in cui una palla viene fatta rimbalzare con un moto che simula quello tipico di una molla o il rimbalzo. Si può vedere il risultato in questa pagina.


Interpolatore

Il punto chiave del nostro interpolatore é che ridefinisce la funzione curve() per determinare il valore della curva risultante come fa più comodo a noi, vediamoci allora il codice del file SpringInterpolator.fx, sostanzialmente uguale a quello che si trova nell'esempio di Netbeans:
package custominterpolator;

import javafx.animation.SimpleInterpolator;
import java.lang.Math;

public class SpringInterpolator extends SimpleInterpolator {
public-init var amplitude = 1.0;
public-init var mass = 0.058;
public-init var stiffness = 12.0;
public-init var phase = 0.0;
public-init var bounce = false;

def pulsation = Math.sqrt(stiffness / mass);

override public function curve(t: Number) : Number {
var t2 = -Math.cos(pulsation*t+phase+Math.PI) * (1-t) * amplitude ;
return if(bounce) 1-Math.abs(t2) else 1-t2;
}
}

A parte i valori utilizzati, e la sinusoidale che non gode di gran successo presso il folto pubblico, il senso della cosa é abbastanza chiaro.

Timeline

Per rendermi il codice più leggibile, ho creato una classe intermedia, Animator, che si occupa di gestire le timeline per i due moti che vogliamo applicare alla nostra palla: un moto a molla, e uno a rimbalzo. Vediamo quindi il codice di Animator.fx:
package custominterpolator;

import javafx.animation.Timeline;

public class Animator {
public var height : Number = 0;
public var y : Number = 0;

def springInterpolator = SpringInterpolator { bounce: false };
def bounceInterpolator = SpringInterpolator { bounce: true };

var sTimeline = Timeline {
keyFrames: [
at(1s) { y => -120 },
at(2.5s) { y => height tween springInterpolator}
]
};

var bTimeline = Timeline {
keyFrames: [
at(1s) { y => -120 },
at(2.5s) { y => height tween bounceInterpolator}
]
};

public function playSpring() : Void {
sTimeline.time = 0s;
sTimeline.playFromStart();
}

public function playBounce() : Void {
bTimeline.time = 0s;
bTimeline.playFromStart();
}
}

Abbiamo due funzioni pubbliche, playSpring() e playBounce() che generano il moto dell'oggetto (stile molla e rimbalzo, rispettivamente) mettendo il risultato nella variabile y con lo scorrere del tempo nella timeline di competenza.

Quelle che deve fare la classe che usa l'Animator, é fornire l'altezza della finestra in input e utilizzare come output l'y risultante quando viene fatta partire l'animazione, per mostrare il movimento dell'oggetto.

Main

Il main.fx della nostra applicazione si occuperà quindi di mettere gli oggetti in scena e di animare la palla collegandola alla y della classe Animator:
package custominterpolator;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.control.Button;
import javafx.scene.text.Text;
import javafx.scene.shape.Circle;
import javafx.scene.effect.DropShadow;

var width : Number = bind scene.width;
var height : Number = bind scene.height;

var description = Text {
content: "Spring vs. Bounce Interpolator"
fill: Color.WHITE
y: 20, translateX: bind (width-150)/2
}

def line = Line {
startX: 0, startY: bind height - 150,
endX: bind width, endY: bind height - 150
stroke: Color.GRAY
}

def btnSpring = Button {
translateX: 10, translateY: bind height - 40, width: 60
text: "Spring"
action: function() { animator.playSpring(); }
}

def btnBounce = Button {
translateX: 10, translateY: bind height - 70, width: 60
text: "Bounce"
action: function() { animator.playBounce(); }
}

def ball = Circle {
centerX: bind width/2, centerY: bind height - 150, radius: 30
translateY: bind animator.y;
fill: Color.GREEN;
effect: DropShadow { offsetX: 3, offsetY: 3 }
}

def animator = Animator {
height: height;
}

def scene : Scene = Scene {
width: 240, height: 320
fill: Color.PURPLE
content: [ description, line, btnSpring, btnBounce, ball ]
}

def stage = Stage {
title: "Spring Animation"
scene: scene
}

Animazione al contrario

L'ultimo articolo correntemente pubblicato nella sezione Animation della pagina Learn di JavaFX, é intitolato Reverse Animation Easily Using the Rate Trick ed é stato scritto da Nancy Hildebrandt.

Lo scopo é quello di mostrarci come si possa usare la variabile rate della classe Timeline per ottenere molto facilmente animazioni al contrario.

Al solito, applicherò qualche piccola variazione al codice proposto nell'articolo, ad esempio uso bottoni standard, invece di crearli dalle forme geometriche di base, e cerco in genere di semplificare il più possibile il codice.

Si può vedere l'applicazione in esecuzione in questa pagina.

In pratica il campo rate della timeline agisce da moltiplicatore per la velocità del nodo legato. E quindi, assegnandogli un valore negativo otteniamo una inversione del moto.

La timeline

Dopo aver definito due costanti, startX e endX, che definiscono il perrcorso del nostro oggetto, e due variabili che tengono traccia della sua posizione corrente e della sua direzione di movimento.

Definiamo allora una timeline il cui rate é legato alla direzione del moto, su cui agiremo con i bottoni:

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

def startX = 100.0;
def endX = 250.0;

var currX = startX;
var forward = true;

def timeline = Timeline { //Timeline for animation of circle
rate: bind (if(forward) 1 else -1)
keyFrames: at(3s) { currX => endX tween Interpolator.LINEAR }
};

Il bottone "go"

Il bottone "go" chiede di muovere l'oggetto in avanti. La direzione in avanti forward é perciò "vera" e si chiama il metodo play() per la timeline. Nel caso ci si trovi a fine corsa, si riporta l'oggetto all'inizio della sua traiettoria e si riparte.

import javafx.scene.control.Button;
...
def go = Button {
translateX: 80, translateY: 140, width: 80
text: "Go"
action: function() {
forward = true;
if(currX == endX)
timeline.playFromStart()
else
timeline.play();
}
}

Il bottone "back"

Il bottone "back" chiede di muovere l'oggetto indietro. Normalmente assegneremo a forward il valore false. Ma c'é una eccezione: quando siamo a inizio traiettoria. In questo caso non possiamo, come nel caso "go", portare l'oggetto dall'altra parte e dare corso al comando richiesto, dato che playFromStart() non permette questo comportamento, ci conviene invece interpretare il comando "back" come se fosse un "go".

Ma c'é un'altra piccola complicazione. Probabilmente a causa di un bug, quando si alternano inversioni di direzione può accadere che il termine del movimento via "back" non porti esattamente alla posizione iniziale, ma a una frazione di unità da essa.

Per questo motivo non si controlla che la posizione corrente sia uguale a quella iniziale ma che la loro differenza sia inferiore all'unità.

def back = Button {
translateX: 180, translateY: 140, width: 80
text: "Back"
action: function() {
forward = if(currX - startX > 1) true else false;
timeline.play();
}
}

Ci resta da definire l'oggetto che vogliamo muovere, un cerchio, e metterlo, insieme ai bottoni che ne regolano il movimento, sulla scena.

La cosa principale da notare nel codice che definisce il cerchio e che il suo centro é legato a currX, la variabile modificata dalla timeline.

Vediamo quindi qui a seguire il codice completo dello script:
package reverseanimation;

import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.control.Button;
import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;
import javafx.scene.effect.DropShadow;

def startX = 100.0;
def endX = 250.0;

var currX = startX; // current x for the circle
var forward = true; // current movement direction

def timeline = Timeline { //Timeline for animation of circle
rate: bind (if(forward) 1 else -1)
keyFrames: at(3s) { currX => endX tween Interpolator.LINEAR }
};

def go = Button {
translateX: 80, translateY: 140, width: 80
text: "Go"
action: function() {
forward = true;
if(currX == endX)
timeline.playFromStart()
else
timeline.play();
}
}

def back = Button {
translateX: 180, translateY: 140, width: 80
text: "Back"
action: function() {
forward = if(currX - startX > 1) true else false;
timeline.play();
}
}

def mover = Circle {
centerX: bind currX, centerY: 80, radius: 20
fill: Color.GREEN
effect: DropShadow { offsetX: 3, offsetY: 3 }
}

Stage {
title: "Simple Animation"
width: 350, height: 220
scene: Scene {
content: [ mover, go, back ]
}
}

Capire le trasformazioni

Altro articolo nella sezione Animation della pagina Learn di JavaFX, dedicato questo a una introduzione alle trasformazioni, il titolo é Understanding Transformations.

JavaFX fornisce un notevole supporto alle funzionalità per la trasformazione di oggetti UI. Come in molte librerie grafiche, le tre principali possibilità di trasformare un oggetto sono relative alla traslazione, scalabilità e rotazione . Ce ne sono poi altre, ma é importante avere una buona conoscenza di queste tre, che sono alla base delle altre possibili trasformazioni.

Il risultato dei nostri esperimenti sarà questa serie di oggetti variamente colorati posti sulla nostra scena ognuno dei quali, tranne il quadrato nero in alto a sinistra, sono modificati per mezzo di traslazione, cambio scala o rotazione, rispetto a un nodo.

Traslazione

Per traslare un oggetto si può assegnare un valore ai suoi attributi translateX e translateY, valori positivi spostano l'oggetto verso destra e il basso rispettivamente.
Nell'esempio il rettangolo rt viene spostato verso destra e verso il basso di 100 pixel rispetto a r:

def r = Rectangle {
x: 30, y: 10, width: 20, height: 20
}

def rt = Rectangle {
x: 30, y: 10, width: 20, height: 20
translateX: 100, translateY: 100
fill: Color.BROWN
}

Cambio scala

Per far crescere un nodo sulla scena basta agire sulle sue proprietà scaleX e scaleY che agiscono da moltiplicatori per la larghezza e l'altezza, rispettivamente.

Si noti che, al contrario di quanto specificato nell'articolo, il punto fermo dell'oggetto da cui si applica la trasformazione é il suo centro. Probabilmente questo vale per JavaFX 1.2, e nella versione precedente non era così.

Per verificare il comportamento di questa funzionalità ho creato quattro rettangoli in questo modo:

def r12 = Rectangle {
x: 80, y: 10, width: 20, height: 20
scaleX: 2
fill: Color.DARKBLUE;
}

def r13 = Rectangle {
x: 30, y: 40, width: 20, height: 20
scaleX: 2
fill: Color.DARKBLUE;
}

def r31 = Rectangle {
x: 30, y: 80, width: 20, height: 20
fill: Color.DARKCYAN;
}

def r32 = Rectangle {
x: 80, y: 80, width: 20, height: 20
scaleX: 2
scaleY: 2
fill: Color.DARKCYAN;
}

Rotazione

Per far rotare un nodo sulla scena basta assegnare il numero di gradi della rotazione all'attribuito rotate dell'oggetto. Si noti che la rotazione avviene rispetto al centro dell'oggetto stesso.

def rr = Rectangle {
x: 130, y: 10, width: 20, height: 20
fill: Color.AQUA
rotate: 45
}

Insetti luminosi in volo

Continuiamo a lavorare sulla serie di articoli sull'animazione indicati nella sezione Learn di JavaFX.

Vediamo ora quello titolato "Using Paths and Timelines Together (Firefly)"

Scopo dell'applicazione é quello di usare le classi Timeline e Path per creare un fondale su cui si muovono in modo casuale una quantità di cerchi (a simboleggiare degli insetti) che si accendono e spengono in modo anch'esso causale.

Si può vedere il codice in esecuzione in questa pagina.

Il codice originale é organizzato in quattro file:
  • Main: dichiara lo stage e la scena su cui agiscono gli insetti;
  • Fly: classe che estende CustomNode per definire i nostri insetti;
  • Lighter: classe per dire a un insetto quando si deve illuminare;
  • Flight: classe per definire il volo casuale di un insetto

Usare due classi per definire il moto dell'insetto m'é sembrato sovrabbondante, e perciò ho spostato il codice di Lighter in Flight, che ora si occupa di tutte le caratteristiche del volo.

Poi ho cambiato il moto degli insetti, in originale si muovono tracciando una cubica Bezier (classe CubicCurveTo), io ho preferito una semplice retta, ispirandomi al volo della mosca; ho ridotto il numero degli oggetti volanti da venti a sei, e ho fatto altre piccole modifiche.

Insetto: prima versione

Diamo una prima semplice definizione della classe Fly, che rappresenta il nostro insetto, al fine di permetterci di scrivere il main del nostro progetto:

package fireflies;

import javafx.scene.CustomNode;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;

public class Fly extends CustomNode {
public-init var where: Rectangle;

def fly: Circle = Circle {
radius: 6, strokeWidth: 2, stroke: Color.AQUAMARINE
}

override function create() : Node {
return fly
}
}

La variabile where, che identifica l'area in cui é confinato al volo l'insetto, al momento non é utilizzata.

Stage e scena

Nella scena dello stage mettiamo un rettangolo e gli insetti che sono creati "on the fly" (é il caso di dirlo) con un ciclo for.

package fireflies;

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

def area = Rectangle {
x: 0, y: 0, height: 400, width: 400
fill: Color.DARKBLUE
}

Stage {
title: "Fire Flies"
resizable: false
scene: Scene {
fill: Color.BLACK
content: [
area,
for(i in [1..6]) { Fly { where: area } }
]
}
}

Interessante notare che Main.fx é già completo. La logica é tutta nella classe Flight che viene pilotata dalla classe Fly.

Il volo: prima versione

La classe Flight richiede che vengano inizializzate queste variabili:
  • fly: l'insetto corrente, oggetto del volo
  • maxX: massima ascissa raggiungibile in volo
  • maxY: massima ordinata
  • min: minima ascissa/ordinata

E rende disponibile flyColor, che é il colore dell'insetto, che faremo variare nel tempo.

Definiamo poi tre variabili private:
  • randomLightTime: tempo in cui l'insetto non s'illumina
  • endX: ascissa del punto terminale del percorso corrente
  • endY: ordinata del punto terminale del percorso corrente


Definiamo infine due funzioni, che al momento non fanno nulla, ma che gestiranno il movimento e l'illuminazione dell'insetto:

import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.util.Math;

public class Flight {
public-init var fly : Node;

public-init var maxX;
public-init var maxY;
public-init var min : Number;

public var flyColor = Color.BLACK;

var randomLightTime : Duration = Duration.valueOf((3000 * Math.random()) + 2000);
var endX = maxX/2;
var endY = maxY/2;

public function createAndPlayAnimation() : Void {
}

public function startLighter() : Void {
}
}


Insetto completo

Aggiungiamo il colore riempitivo dell'insetto, che é legato al colore come viene determinato nella classe Flight.
Definiamo il volo, passando i parametri richiesti. Nota che max e min sono generati in modo che l'insetto non possa attraversare i limiti del rettangolo in cui é ristretto.
La funzione create(), prima di ritornare l'insetto, fa partire il volo e la gestione del colore.

public class Fly extends CustomNode {
public-init var where: Rectangle;

def fly: Circle = Circle {
radius: 6, strokeWidth: 2, stroke: Color.AQUAMARINE
fill: bind flight.flyColor;
}

def flight = Flight {
fly: fly;
maxX: where.width - fly.radius;
maxY: where.height - fly.radius;
min: fly.radius;
}

override function create() : Node {
flight.createAndPlayAnimation();
flight.startLighter();
return fly
}
}


Il volo dell'insetto

Vediamo la funzione createAndPlayAnimation() nella classe Flight.

Generiamo il punto iniziale e finale del percorso e generiamolo usando la classe PathTransition, nota che la durata di questo tratto di volo é casuale, ma dura almeno un decimo di secondo (per evitare movimenti eccessivamente veloci), nel campo action viene specificata la funzione da eseguire al termine dell'esecuzione della funzione corrente - che é poi ancora la stessa.


import javafx.animation.transition.AnimationPath;
import javafx.animation.transition.PathTransition;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.LineTo;

...
public function createAndPlayAnimation() : Void {
def startX = endX;
def startY = endY;

endX = Math.max(min, maxX * Math.random());
endY = Math.max(min, maxY * Math.random());

PathTransition {
node: fly;
path: AnimationPath.createFromPath(
Path {
elements: [
MoveTo { x: startX, y: startY },
LineTo { x: endX, y: endY }
]
}
)

duration: Duration.valueOf(100 + (1000 * Math.random()))
action: function() {createAndPlayAnimation();}
}.play();
}

Aggiungendo questa funzione la nostra applicazione prende vita e i nostri insetti si metto a volare per tutto il loro spazio.

Luci in volo

Scriviamo il codice della startLighter() nella classe Flight.

In realtà é una funzione piuttosto semplice: crea un timeline e la fa partire.
La timeline si ripete in modo indefinito con autoreverse.
Da notare che per la definizione dei KeyFrame non si é potuta usare la costruzione semplificata "at() ..." dato che come tempo vogliamo usare una variabile, mentre l'at richiede una duration letterale.


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

public function startLighter() : Void {
Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
KeyFrame {
time: randomLightTime
values: [ flyColor => Color.BLACK ]
},
KeyFrame {
time: randomLightTime + .5s;
values: [ flyColor => Color.YELLOW tween Interpolator.EASEBOTH ]
}
];
}.play();
}

Con questa funzione il nostro programma é completo.

Più timeline per un oggetto

L'articolo Basic Timeline Animation nella sezione Animation di JavaFX Learn mostra un esempio semplice ma di effetto su come animare un oggetto in due dimensioni, usando due differenti timeline.

Come bonus noi ci aggiungiamo pure una variazione delle dimensioni nostro oggetto, gestita da una terza timeline.

Si può vedere l'applicazione in esecuzione in questa pagina.

Stage, scena e attore

Definiamo uno stage con la sua scena e mettiamo al suo interno un cerchio:

package timeline;

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

def circle = Circle {
centerX: 50, centerY: 50, radius: 10
fill: Color.DARKORCHID
}

Stage {
title: "Animator"

scene: Scene {
width: 100, height: 100
fill: Color.LIGHTGREEN
content: circle
}
}


Doppia azione

Definiamo ora due timeline che operano su due variabili numeriche, x e y, assegnando a ognuna delle due un valore crescente tra 10 e 90, la timeline continurà indefinitivamente e, ogni volta che viene raggiunto un estremo, si invertirà la direzione di crescita (come dire: la sequenza sarà ... 88, 89, 90, 89, 88 ...), usiamo l'interpolatore EASEBOTH così, al raggiungere degli estremi, il movimento risulta rallentato. Notiamo ancora che i tempi impiegati per compiere il ciclo sono differenti tra le due timeline, questo renderà il movimento lungo x e y più indipendente col passar del tempo.
Notiamo infine che le timeline sono fatte partire immediatamente:

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

var x = 10;
var y = 10;

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (1.5s) {x => 90 tween Interpolator.EASEBOTH}
}.play();

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (2.7s) {y => 90 tween Interpolator.EASEBOTH}
}.play();

Per rendere ancora più bizzarra la situazione, aggiungiamo una terza timeline che regolerà il raggio del nostro cerchio, facendolo variare tra 5 e 10, in mezzo secondo e in modo lineare:

var r = 5;
...
Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (0.5s) {r => 10 tween Interpolator.LINEAR}
}.play();


Ci resta ora di associare x, y e r al cerchio, in modo che si muova e cambi dimensione al loro variare:

def circle = Circle {
centerX: bind x, centerY: bind y, radius: bind r
...
}

E così il nostro codice é completo, rivediamolo qui a seguire:

package timeline;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;

var x = 10;
var y = 10;
var r = 5;

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (1.5s) {x => 90 tween Interpolator.EASEBOTH}
}.play();

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (2.7s) {y => 90 tween Interpolator.EASEBOTH}
}.play();

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: at (1s) {r => 10 tween Interpolator.LINEAR}
}.play();

def circle = Circle {
centerX: bind x, centerY: bind y, radius: bind r
fill: Color.DARKORCHID
}

Stage {
title: "Animator"

scene: Scene {
width: 100, height: 100
fill: Color.LIGHTGREEN
content: circle
}
}

Animare su un percorso arbitrario

Sempre nella pagina Learn di JavaFX.com, sezione animazione, passo all'articolo Animation Along an Arbitrary Path di Peter Zhelezniakov.

Si tratta qui dell'uso della classe javafx.animation.transition.PathTransition che permette una delle transizioni più sofisticate tra quelle disponibili in JavaFX. Nonostante ciò é anche una classe facile da usare: si crea un percorso, un oggetto che lo deve percorrere, li si passa alla PathTransition che fornisce la logica per l'animazione.

Il codice completo sviluppato da Peter viene messo a disposizione qui. Teniamolo da traccia e sviluppiamo passo dopo passo l'applicazione.

Il risultato finale viene mostrato qui.

Finestra principale

Peter ha evidentemente un profondo background in java, e la strutturazione del suo codice lo riflette. La cosa più evidente é che Peter, invece di lasciare a JavaFX il compito di interpolare la funzione run() da chiamare al momento dell'esecuzione, creando lo stage e le funzioni solitamente sparse nello script, preferisce esplicitarla. Seguiamo volentieri la sua impostazione, creandoci lo stage e la scena in questo modo:

package pathanim;

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

function run(): Void {
Stage {
title: "Path Animation"
scene: Scene {
width: 400, height: 300
}
};
}

Oggetto animato

Creamo un oggetto che faremo muovere lungo un percorso:

import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

def bar1 = Rectangle {
width: 5, height: 50
fill: Color.RED
};

Il percorso

E questo é il percorso che vogliamo che il nostro oggetto segua:

import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;

def track1 = Path {
stroke: Color.BLUE
strokeWidth: 4
elements: [
MoveTo { x: 50, y: 100 },
CubicCurveTo {
controlX1: 100, controlY1: -50
controlX2: 300, controlY2: 250
x: 350, y: 100
}
]
};

Specifichiamo il punto iniziale con MoveTo, e poi il percorso é la cubica definita da CubicCurveTo.

L'animazione

Ci pensa PathTransition ad associare oggetto a percorso, usando i parametri passati per definire le modalità dell'animazione:

def anim1 = PathTransition {
node: bar1
path: AnimationPath.createFromPath(track1)
orientation: OrientationType.NONE
interpolator: Interpolator.LINEAR
duration: 5s
repeatCount: Timeline.INDEFINITE
};

Come orientamento abbiamo specificato OrientationType.NONE che é il default e che significa che l'oggetto si muove lungo il percorso senza cambiare orientamento. Più interessante é OrientationType.ORTHOGONAL_TO_TANGENT, in cui l'oggetto trasla come farebbe chiunque seguendo un percorso, seguendo il proprio naso. Il secondo oggetto sul secondo percorso (vedi codice completo più sotto) segue appunto questa modalità.

Esecuzione

Aggiorniamo la funzione run() in modo da mettere sulla scena i nostri oggetti e far partire l'animazione:

function run(): Void {
Stage {
title: "Path Animation"
scene: Scene {
width: 400, height: 300
content: [ track1, bar1 ]
}
};

anim1.play();
}

Codice completo

Nel codice completo raddoppiamo gli oggetti in scena:

package pathanim;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.animation.transition.AnimationPath;
import javafx.animation.transition.OrientationType;
import javafx.animation.transition.PathTransition;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.Rectangle;

def bar1 = Rectangle {
width: 5, height: 50
fill: Color.RED
};

def bar2 = Rectangle {
width: 5, height: 50
fill: Color.BLUE
};

def track1 = Path {
stroke: Color.BLUE
strokeWidth: 4
elements: [
MoveTo { x: 50, y: 100 },
CubicCurveTo {
controlX1: 100, controlY1: -50
controlX2: 300, controlY2: 250
x: 350, y: 100
}
]
};

def track2 = Path {
stroke: Color.RED
strokeWidth: 4
elements: [
MoveTo { x: 50, y: 200 },
CubicCurveTo {
controlX1: 100, controlY1: 50
controlX2: 300, controlY2: 350
x: 350, y: 200
}
]
};

def anim1 = PathTransition {
node: bar1
path: AnimationPath.createFromPath(track1)
orientation: OrientationType.NONE
interpolator: Interpolator.LINEAR
duration: 5s
repeatCount: Timeline.INDEFINITE
};

def anim2 = PathTransition {
node: bar2
path: AnimationPath.createFromPath(track2)
orientation: OrientationType.ORTHOGONAL_TO_TANGENT
interpolator: Interpolator.LINEAR
duration: 5s
repeatCount: Timeline.INDEFINITE
};

function run(): Void {
Stage {
title: "Path Animation"
scene: Scene {
width: 400, height: 300
content: [ track1, track2, bar1, bar2 ]
}
};

anim1.play();
anim2.play();
}

Le basi dell'animazione

All'interno della sezione Learn di JavaFX.com si trova una utile serie di articoli che offrono interessanti spunti di approfondimento sul linguaggio.

In questa serie di post trattiamo gli articoli che si riferiscono all'animazione di oggetti con JavaFX.

Nell'articolo Animation Basics for JavaFX Beginners di Nancy Hildebrandt, si offre una introduzione molto lineare alle basi dell'animazione. In particolare si mostra come animare, muovendo da un punto ad un altro, in modo concorrente e sequenziale, semplici oggetti.

In questo post prendiamo l'articolo e lo variamo leggermente secondo il nostro opinabile gusto.

Il risultato finale viene mostrato qui.

Creare un rettangolo

Prima cosa, creiamo un istanza della classe Rectangle che, in realtà, ci darà un quadrato.

package basicanimation;

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

def redSquare = Rectangle {
x: 20, y: 60
width: 20, height: 20
fill: Color.RED
}

Stage {
title: "One Red Square"
width: 240, height: 200

scene: Scene {
content: redSquare
}
}

Al solito, ci siamo creati stage e scena, e dentro ci siamo messi l'oggetto che ci siamo precedentemente definiti. In questo caso redSquare.

Ci dovrebbe essere ormai ben chiaro che lo stage definisce la finestra principale della nostra applicazione e la scena contiene gli oggetti che ne fanno l'azione.
Avessimo dubbi su questi concetti chiave, ci converrebbe rivederci i tutorial di base core e ui.

Un po' di animazione

Vogliamo ora far muovere il nostro rettangolo dal suo punto iniziale x:20 a x:100, in 3 secondi netti.
Per far questo usiamo la classe Timeline, implementandone un oggetto in questo modo:

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

var redSlider: Integer;

Timeline {
keyFrames: [
KeyFrame {
time: 0s
values: [ redSlider => 20 ]
}
KeyFrame {
time: 3s
values: [ redSlider => 100 tween Interpolator.LINEAR ]
}
]
}.play();

def redSquare = Rectangle {
x: bind redSlider, y: 60
...
}

La nostra timeline ha due stati, istanze della classe KeyFrame, quello iniziale che é la "foto" della situazione all'istante zero e determina che il valore della variabile redSlider, che abbiamo appositamente dichiarato sopra, sia 20. Il secondo stato é rappresenta la situazione al terzo secondo, e ci dice che redSlider vale a quel momento 100. Quello che succede da 0 a 3 ci viene detto da tween, che sarebbe poi l'operatore che genera tutti gli stati intermedi usando l'interpolazione specificata, nel nostro caso lineare.

Dopo che abbiamo definito il nostro oggetto timeline, che va a impattare su redSlider, abbiamo modificato la definizione di redSquare, in modo che l'ascissa del nostro rettangolo sia legata a redSlider, e quindi vari nel tempo.

Risultato: redSquare si muove in accordo con quanto definito in timeline.

Timeline succinta

Chi ha un background in linguaggi che amanno la concisione, apprezzerà particolarmente la possibilità di utilizzare l'operatore at nella definizione di una timeline che, combinato al fatto che si può evitare di specificare cosa succede all'istante zero in una timeline, sempre che definiamo opportunamente la variabili che vengono impattate, ci permette di riscrivere il codice sovrastante in modo decisamente più snello:

var redSlider: Integer = 20;

Timeline {
keyFrames: at (3s) { redSlider => 100 tween Interpolator.LINEAR }
}.play();

Non male, davvero.

Movimento simultaneo

Mettiamo in scena un secondo attore, un quadrato nero, che si muove in parallelo al rosso:

def blackSquare = Rectangle {
translateX: bind redSlider, y: 90
width: 20, height: 20
fill: Color.BLACK
}

Stage {
title: "Moving around"
...

scene: Scene {
content: [redSquare, blackSquare]
}
}

Ma un timeline può controllare molteplici linee di sviluppo nel tempo, approfittiamone per muovere blackSquare in modo indipendente da redSquare:

...
var blackSlider: Integer = 110;

Timeline {
keyFrames: [
at (3s) { redSlider => 100 tween Interpolator.LINEAR }
at (3s) { blackSlider => 200 tween Interpolator.EASEBOTH }
]
}.play();

def blackSquare = Rectangle {
translateX: bind blackSlider, y: 60
...
}

Nota che in keyFrames ora c'é un array di elementi, che quindi sono inclusi tra parentesi quadre. Per il movimento del rettangolo nero ho usato l'interpolazione EASEBOTH, giusto per variare.

Animazione sequenziale

Vogliamo ora usare la nostra timeline per animare oggetti diversi in tempi diversi. Aggiungiamo una lineetta e una palla verde sulla scena:

import javafx.scene.shape.Line;
import javafx.scene.shape.Circle;

def line = Line {
startX: 110, startY: 40
endX: 110, endY: 100
strokeWidth: 1
stroke: Color.BLACK
}

def ball = Circle {
translateX: 110, centerY: 70, radius: 10
fill: Color.GREEN
}

Stage {
...
scene: Scene {
content: [redSquare, blackSquare, line, ball]
}
}

E adesso animiamo la palla. Definiamo ballSlider con la posizione iniziale della nostra palla che, come descritto in Timeline, non fa nulla fino al quarto secondo, quando inizia il suo moto lineare verso la sua destinazione x:10, dove arriverà in 3 secondi.

...
var ballSlider: Integer = 110;

Timeline {
keyFrames: [
...
at (4s) { ballSlider => 110 }
at (7s) { ballSlider => 10 tween Interpolator.LINEAR }
]
}.play();
...
def ball = Circle {
translateX: bind ballSlider, centerY: 70, radius: 10
...
}

Animation reloaded

Aggiungiamo un bottone per far ripartire l'animazione. Evitiamo la complicazione di costruirci un bottone custom e usiamone invece uno standard, l'azione associata al bottone é la playFromStart() della timeline, che rendiamo perciò disponibile come oggetto:

import javafx.scene.control.Button;
...
def timeline = Timeline {
...
};
timeline.play();
...
def button = Button {
layoutX: 80, layoutY: 130
text: "Reload"
action: function() { timeline.playFromStart(); }
}

Stage {
...
scene: Scene {
content: [redSquare, blackSquare, line, ball, button]
}
}

E con questo la nostra applicazione é completa. Segue il listato del codice che abbiamo sviluppato:

package basicanimation;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.shape.Line;
import javafx.scene.shape.Circle;
import javafx.scene.control.Button;

var redSlider: Integer = 20;
var blackSlider: Integer = 110;
var ballSlider: Integer = 110;

def timeline = Timeline {
keyFrames: [
// first step: squares move
at (3s) { redSlider => 100 tween Interpolator.LINEAR }
at (3s) { blackSlider => 200 tween Interpolator.EASEBOTH }
// second step: ball moves
at (4s) { ballSlider => 110 }
at (7s) { ballSlider => 10 tween Interpolator.LINEAR }
]
};
timeline.play();

def redSquare = Rectangle {
x: bind redSlider, y: 60
width: 20, height: 20
fill: Color.RED
}

def blackSquare = Rectangle {
translateX: bind blackSlider, y: 60
width: 20, height: 20
fill: Color.BLACK
}

def line = Line {
startX: 110, startY: 40
endX: 110, endY: 100
strokeWidth: 1
stroke: Color.BLACK
}

def ball = Circle {
translateX: bind ballSlider, centerY: 70, radius: 10
fill: Color.GREEN
}

def button = Button {
layoutX: 80, layoutY: 130
text: "Reload"
action: function() { timeline.playFromStart(); }
}

Stage {
title: "Moving around"
width: 240, height: 200

scene: Scene {
content: [redSquare, blackSquare, line, ball, button]
}
}