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

Nessun commento:

Posta un commento