/**
* Copyright 2013 (C) Mr LoNee - (Laurent NICOLAS) - www.mrlonee.com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package com.mrlonee.radialfx.moviemenu;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.ParallelTransition;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.effect.BlendMode;
import javafx.scene.effect.Glow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CircleBuilder;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.util.Duration;
import com.mrlonee.radialfx.core.RadialMenuItem;
import com.mrlonee.radialfx.core.RadialMenuItemBuilder;
public class RadialMovieMenu extends Group {
private double itemInnerRadius = 60;
private double itemRadius = 95;
private double centerClosedRadius = 28;
private double centerOpenedRadius = 40;
private String[] menus;
private Circle center;
private final List<RadialMenuItem> items;
private final Group itemsGroup = new Group();
private final Group textsGroup = new Group();
private Circle fakeBackground;
private Text centerText;
private Circle radiusStroke;
private Circle innerRadiusStroke;
private final Color centerColor = Color.web("ffffff");
private final Color itemColor = Color.web("ffffff80");
private final Paint textColor = Color.web("000000a0");
private Paint strokeColor = Color.web("c0c0c0");
final Font textFont = Font.font(java.awt.Font.SANS_SERIF,FontWeight.NORMAL, 11);
final Font textFontBold = Font.font(java.awt.Font.SANS_SERIF,FontWeight.BOLD, 11);
final Font menuFont = Font.font(java.awt.Font.SANS_SERIF, FontWeight.BOLD,12);
private double animDuration = 350;
private Animation openTransition;
private final Map<RadialMenuItem, List<Text>> itemToTexts;
public RadialMovieMenu(final String[] itemNames,
final double innerRadius,
final double radius, final double centerClosedRadius,
final double centerOpenedRadius) {
menus = itemNames;
itemInnerRadius = innerRadius;
itemRadius = radius;
this.centerClosedRadius = centerClosedRadius;
this.centerOpenedRadius = centerOpenedRadius;
itemToTexts = new HashMap<RadialMenuItem, List<Text>>();
items = new ArrayList<RadialMenuItem>();
double menuLetterNumber = 0;
for (final String itemTitle : menus) {
menuLetterNumber += itemTitle.length();
}
double startAngle = 0;
radiusStroke = CircleBuilder.create().radius(0).stroke(strokeColor)
.fill(null).build();
innerRadiusStroke = CircleBuilder.create().radius(0).fill(null)
.stroke(strokeColor).build();
itemsGroup.getChildren().addAll(radiusStroke, innerRadiusStroke);
for (final String itemTitle : menus) {
final double length = 360.0 * (itemTitle.length() / menuLetterNumber);
final RadialMenuItem item = RadialMenuItemBuilder.create()
.backgroundFill(itemColor).innerRadius(0).radius(1)
.strokeVisible(false).offset(0).startAngle(startAngle)
.length(length).build();
items.add(item);
itemsGroup.getChildren().add(item);
final List<Text> texts = getTextNodes(itemTitle, startAngle);
textsGroup.getChildren().addAll(texts);
itemToTexts.put(item, texts);
final EventHandler<? super MouseEvent> itemEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(final MouseEvent event) {
if (event.getEventType() == MouseEvent.MOUSE_ENTERED) {
for (final Text charText : texts) {
charText.setFill(Color.BLACK);
charText.setFont(textFontBold);
}
} else if (event.getEventType() == MouseEvent.MOUSE_EXITED) {
for (final Text charText : texts) {
charText.setFill(textColor);
charText.setFont(textFont);
}
}
}
};
item.setOnMouseEntered(itemEventHandler);
item.setOnMouseExited(itemEventHandler);
item.setOnMouseClicked(itemEventHandler);
startAngle += length;
}
center = CircleBuilder.create().fill(centerColor)
.radius(centerClosedRadius).stroke(strokeColor).centerX(0)
.centerX(0).build();
centerText = new Text("MENU");
centerText.setFont(menuFont);
centerText.setFontSmoothingType(FontSmoothingType.LCD);
final StackPane stack = new StackPane();
stack.getChildren().addAll(center, centerText);
stack.translateXProperty().bind(stack.widthProperty().divide(-2.0));
stack.translateYProperty().bind(stack.heightProperty().divide(-2.0));
final EventHandler<? super MouseEvent> expansionEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(final MouseEvent event) {
if (event.getEventType() == MouseEvent.MOUSE_ENTERED) {
openTransition = createOpenTransition();
openTransition.play();
} else if (event.getEventType() == MouseEvent.MOUSE_EXITED) {
if (openTransition != null) {
Duration startDuration = Duration.millis(animDuration);
if (openTransition.getStatus() == Status.RUNNING) {
openTransition.stop();
startDuration = openTransition.getCurrentTime();
}
openTransition.setAutoReverse(true);
openTransition.setCycleCount(2);
openTransition.playFrom(startDuration);
}
}
}
};
fakeBackground = CircleBuilder.create().fill(Color.TRANSPARENT)
.radius(centerClosedRadius + 4).centerX(0).centerX(0).build();
setOnMouseEntered(expansionEventHandler);
setOnMouseExited(expansionEventHandler);
getChildren().addAll(fakeBackground, itemsGroup, textsGroup, stack);
}
private Animation createOpenTransition() {
final ParallelTransition openTransition = new ParallelTransition();
final Animation centerTransition = new Timeline(new KeyFrame(
Duration.ZERO, new KeyValue(center.radiusProperty(),
centerClosedRadius), new KeyValue(
fakeBackground.radiusProperty(),
fakeBackground.getRadius())), new KeyFrame(
Duration.millis(animDuration), new KeyValue(
center.radiusProperty(), centerOpenedRadius),
new KeyValue(fakeBackground.radiusProperty(), itemRadius + 4)));
openTransition.getChildren().add(centerTransition);
// Text font transition
final DoubleProperty animValue = new SimpleDoubleProperty();
final ChangeListener<? super Number> listener = new ChangeListener<Number>() {
@Override
public void changed(
final ObservableValue<? extends Number> obsValue,
final Number previousValue, final Number newValue) {
final Font f = getTextFont(newValue.doubleValue());
centerText.setFont(f);
}
Font[] fonts = new Font[] {
Font.font(java.awt.Font.SANS_SERIF, FontWeight.BOLD, 12),
Font.font(java.awt.Font.SANS_SERIF, FontWeight.BOLD, 13),
Font.font(java.awt.Font.SANS_SERIF, FontWeight.BOLD, 14),
Font.font(java.awt.Font.SANS_SERIF, FontWeight.BOLD, 15),
Font.font(java.awt.Font.SANS_SERIF, FontWeight.BOLD, 16) };
private Font getTextFont(final double newValue) {
final int fontArrayIndex;
if (newValue < 0.2) {
fontArrayIndex = 0;
} else if (newValue < 0.4) {
fontArrayIndex = 1;
} else if (newValue < 0.6) {
fontArrayIndex = 2;
} else if (newValue < 0.8) {
fontArrayIndex = 3;
} else {
fontArrayIndex = 4;
}
return fonts[fontArrayIndex];
}
};
animValue.addListener(listener);
final Animation menuTextTransition = new Timeline(new KeyFrame(
Duration.ZERO, new KeyValue(animValue, 0)), new KeyFrame(
Duration.millis(animDuration), new KeyValue(animValue, 1.0)));
openTransition.getChildren().add(menuTextTransition);
final List<KeyValue> keyValueZero = new ArrayList<KeyValue>();
final List<KeyValue> keyValueFinal = new ArrayList<KeyValue>();
for (final RadialMenuItem item : items) {
keyValueZero.add(new KeyValue(item.innerRadiusProperty(),
centerClosedRadius));
keyValueZero.add(new KeyValue(item.radiusProperty(),
centerClosedRadius));
keyValueFinal.add(new KeyValue(item.innerRadiusProperty(),
itemInnerRadius));
keyValueFinal.add(new KeyValue(item.radiusProperty(), itemRadius));
final Animation textTransition = getTextOpenTransition(item);
openTransition.getChildren().add(textTransition);
}
keyValueZero.add(new KeyValue(radiusStroke.radiusProperty(),
centerClosedRadius));
keyValueZero.add(new KeyValue(innerRadiusStroke.radiusProperty(),
centerClosedRadius));
keyValueFinal.add(new KeyValue(radiusStroke.radiusProperty(),
itemInnerRadius));
keyValueFinal.add(new KeyValue(innerRadiusStroke.radiusProperty(),
itemRadius));
final Animation radiusTransition = new Timeline(new KeyFrame(
Duration.ZERO, keyValueZero.toArray(new KeyValue[0])),
new KeyFrame(Duration.millis(animDuration), keyValueFinal
.toArray(new KeyValue[0])));
openTransition.getChildren().add(radiusTransition);
return openTransition;
}
public Animation getTextOpenTransition(final RadialMenuItem item) {
final List<Text> texts = itemToTexts.get(item);
final double textRadius = (itemInnerRadius + itemRadius) / 2.0;
final double startAngle = item.getStartAngle();
final double length = item.getLength() * 0.9;
final double angleOffset = item.getLength() * 0.1;
final double angleStep = (length) / (texts.size() + 1);
for (final Text charText : texts) {
charText.setEffect(null);
charText.setVisible(true);
}
final DoubleProperty animValue = new SimpleDoubleProperty();
final ChangeListener<? super Number> listener = new ChangeListener<Number>() {
@Override
public void changed(
final ObservableValue<? extends Number> obsValue,
final Number previousValue, final Number newValue) {
final double textRotationOffset = 180;
final double radius = centerClosedRadius
+ (textRadius - centerClosedRadius)
* newValue.doubleValue();
double letterAngle = startAngle + angleStep + angleOffset
+ ((1 - newValue.doubleValue()) * textRotationOffset);
final Font f = getTextFont(newValue.doubleValue());
for (final Text charText : texts) {
charText.setRotate(0);
charText.setFont(f);
final Bounds bounds = charText.getBoundsInParent();
final double lettertWidth = bounds.getWidth();
final double lettertHeight = bounds.getHeight();
final double currentX = xCenterOnCircle(letterAngle,
radius, lettertWidth);
final double currentY = yCenterLetterOnCircle(letterAngle,
radius, lettertHeight);
final double rotate = rotate(letterAngle);
charText.setTranslateX(currentX);
charText.setTranslateY(currentY);
charText.setRotate(rotate);
letterAngle += angleStep;
}
}
Font[] fonts = new Font[] {
Font.font(java.awt.Font.SANS_SERIF, FontWeight.NORMAL, 6),
Font.font(java.awt.Font.SANS_SERIF, FontWeight.NORMAL, 7),
Font.font(java.awt.Font.SANS_SERIF, FontWeight.NORMAL, 8),
Font.font(java.awt.Font.SANS_SERIF, FontWeight.NORMAL, 10),
Font.font(java.awt.Font.SANS_SERIF, FontWeight.NORMAL, 11) };
private Font getTextFont(final double newValue) {
final int fontArrayIndex;
if (newValue < 0.2) {
fontArrayIndex = 0;
} else if (newValue < 0.4) {
fontArrayIndex = 1;
} else if (newValue < 0.6) {
fontArrayIndex = 2;
} else if (newValue < 0.8) {
fontArrayIndex = 3;
} else {
fontArrayIndex = 4;
}
return fonts[fontArrayIndex];
}
};
animValue.addListener(listener);
final Animation itemTransition = new Timeline(new KeyFrame(
Duration.ZERO, new KeyValue(animValue, 0)), new KeyFrame(
Duration.millis(animDuration), new KeyValue(animValue, 1.0)));
itemTransition.setOnFinished(new EventHandler<ActionEvent>() {
boolean visible = false;
@Override
public void handle(final ActionEvent event) {
for (final Text charText : texts) {
charText.setEffect(new Glow());
if (visible) {
charText.setVisible(false);
}
}
visible = !visible;
}
});
return itemTransition;
}
private List<Text> getTextNodes(final String title, final double startAngle) {
final List<Text> texts = new ArrayList<Text>();
final char[] titleCharArray = title.toCharArray();
for (int i = titleCharArray.length - 1; i >= 0; i--) {
final Text charText = new Text(
Character.toString(titleCharArray[i]));
charText.setFontSmoothingType(FontSmoothingType.LCD);
charText.setSmooth(true);
charText.setMouseTransparent(true);
charText.setFill(textColor);
charText.setBlendMode(BlendMode.COLOR_BURN);
charText.setFont(textFont);
texts.add(charText);
}
return texts;
}
private double xCenterOnCircle(final double angle, final double radius,
final double width) {
return radius * Math.cos(Math.toRadians(angle)) - width / 2.0;
}
private double yCenterLetterOnCircle(final double angle,
final double radius, final double height) {
return -radius * Math.sin(Math.toRadians(angle)) + height / 4.0;
}
private double rotate(final double angle) {
final double rotate = 90 - angle;
return rotate;
}
}