/* * SPDX-License-Identifier: Apache-2.0 * * Copyright 2016-2020 Gerrit Grunwald. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package eu.hansolo.tilesfx.tools; import javafx.animation.AnimationTimer; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javafx.scene.paint.CycleMethod; import javafx.scene.paint.RadialGradient; import javafx.scene.paint.Stop; import javafx.scene.shape.ArcType; public class RotationEffect extends Region { private static final double PREFERRED_WIDTH = 250; private static final double PREFERRED_HEIGHT = 250; private static final double MINIMUM_WIDTH = 10; private static final double MINIMUM_HEIGHT = 10; private static final double MAXIMUM_WIDTH = 1024; private static final double MAXIMUM_HEIGHT = 1024; private double width; private double height; private double offsetX; private double offsetY; private Canvas canvas; private GraphicsContext ctx; private double angle; private long lastTimerCall; private AnimationTimer timer; private boolean isRunning; private Color _color; private ObjectProperty<Color> color; private double _alpha; private DoubleProperty alpha; private double centerX; private double centerY; private RadialGradient gradient; // ******************** Constructors ************************************** public RotationEffect() { this(Color.WHITE, 0.1, 0.5, 0.5); } public RotationEffect(final Color color, final double alpha, final double centerX, final double centerY) { angle = 0; lastTimerCall = System.nanoTime(); timer = new AnimationTimer() { @Override public void handle(final long now) { if (now > lastTimerCall + 20_000_000l) { redraw(); lastTimerCall = now; } } }; isRunning = false; _color = color; _alpha = alpha; gradient = new RadialGradient(0, 0, 0.5, 0.5, PREFERRED_WIDTH, true, CycleMethod.NO_CYCLE, new Stop(0.0, getColorWithOpacity(getAlpha())), new Stop(1.0, Color.TRANSPARENT)); this.centerX = centerX; this.centerY = centerY; initGraphics(); registerListeners(); } // ******************** Initialization ************************************ private void initGraphics() { if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 || Double.compare(getWidth(), 0.0) <= 0 || Double.compare(getHeight(), 0.0) <= 0) { if (getPrefWidth() > 0 && getPrefHeight() > 0) { setPrefSize(getPrefWidth(), getPrefHeight()); } else { setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT); } } canvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT); ctx = canvas.getGraphicsContext2D(); getChildren().setAll(canvas); } private void registerListeners() { widthProperty().addListener(o -> resize()); heightProperty().addListener(o -> resize()); } // ******************** Methods ******************************************* @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; } @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; } @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); } @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); } @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; } @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; } public void start() { timer.start(); isRunning = true; } public void stop() { timer.stop(); isRunning = false; } public Color getColor() { return null == color ? _color : color.get(); } public void setColor(final Color color) { if (null == this.color) { _color = color; updateGradient(); redraw(); } else { this.color.set(color); } } public ObjectProperty<Color> colorProperty() { if (null == color) { color = new ObjectPropertyBase(_color) { @Override protected void invalidated() { updateGradient(); redraw(); } @Override public Object getBean() { return RotationEffect.this; } @Override public String getName() { return "color"; } }; _color = null; } return color; } public double getAlpha() { return null == alpha ? _alpha : alpha.get(); } public void setAlpha(final double alpha) { if (null == this.alpha) { _alpha = clamp(0.0, 1.0, alpha); updateGradient(); redraw(); } else { this.alpha.set(clamp(0.0, 1.0, alpha)); } } public DoubleProperty alphaProperty() { if (null == alpha) { alpha = new DoublePropertyBase(_alpha) { @Override protected void invalidated() { set(clamp(0.0, 1.0, get())); updateGradient(); redraw(); } @Override public Object getBean() { return RotationEffect.this; } @Override public String getName() { return "alpha"; } }; } return alpha; } public double getCenterX() { return centerX; } public void setCenterX(final double centerX) { this.centerX = clamp(0.0, 1.0, centerX); updateGradient(); redraw(); } public double getCenterY() { return centerY; } public void setCenterY(final double centerY) { this.centerY = clamp(0.0, 1.0, centerY); updateGradient(); redraw(); } private Color getColorWithOpacity(final double alpha) { return Color.color(getColor().getRed(), getColor().getBlue(), getColor().getBlue(), alpha); } private void updateGradient() { gradient = new RadialGradient(0, 0, offsetX + width * centerX, offsetY + height * centerY, 1024, false, CycleMethod.NO_CYCLE, new Stop(0.0, getColorWithOpacity(getAlpha())), new Stop(1.0, Color.TRANSPARENT)); } private double clamp(final double min, final double max, final double value) { if (value < min) { return min; } if (value > max) { return max; } return value; } // ******************** Resizing ****************************************** private void resize() { width = getWidth() - getInsets().getLeft() - getInsets().getRight(); height = getHeight() - getInsets().getTop() - getInsets().getBottom(); if (width > 0 && height > 0) { boolean wasRunning = isRunning; stop(); offsetX = (getWidth() - width) * 0.5; offsetY = (getHeight() - height) * 0.5; updateGradient(); canvas.setWidth(width); canvas.setHeight(height); if (wasRunning) { start(); } else { redraw(); } } } private void redraw() { ctx.clearRect(0, 0, width, height); boolean toggle = true; double x = -width - width * 0.5 + width * getCenterX(); double y = -height - height * 0.5 + height * getCenterY(); double w = 3 * width; double h = 3 * height; for (int i = 0 ; i < 360 ; i += 15) { ctx.setFill(toggle ? gradient : Color.TRANSPARENT); ctx.fillArc(x, y, w, h, -(i + angle), 15, ArcType.ROUND); toggle ^= true; } angle += 2; if (angle > 360) { angle = 0; } } }