在 JavaFX 画布上绘制大量动画形状时如何优化渲染性能

Hua*_*uae 5 graphics performance javafx canvas draw

我在 HTML5 演示中使用 Javafx 实现相同的效果,但使用 Javafx 的帧速率非常低

我如何优化它,我是否写错了或者是否有其他更好的方法来实现它。

这是原始的 html5 演示项目:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>snows</title>
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            overflow: hidden;
        }

        .container {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }
    </style>
</head>

<body>
    <div id="jsi-snow-container" class="container"></div>

    <script>
        var RENDERER = {
            SNOW_COUNT: { INIT: 100, DELTA: 1 },
            BACKGROUND_COLOR: 'hsl(%h, 50%, %l%)',
            INIT_HUE: 180,
            DELTA_HUE: 0.1,
            init: function () {
                this.setParameters();
                this.reconstructMethod();
                this.createSnow(this.SNOW_COUNT.INIT * this.countRate, true);
                this.render();
            },
            setParameters: function () {
                this.$window = $(window);
                this.$container = $('#jsi-snow-container');
                this.width = this.$container.width();
                this.height = this.$container.height();
                this.center = { x: this.width / 2, y: this.height / 2 };
                this.countRate = this.width * this.height / 500 / 500;
                this.canvas = $('<canvas />').attr({ width: this.width, height: this.height }).appendTo(this.$container).get(0);
                this.context = this.canvas.getContext('2d');
                this.radius = Math.sqrt(this.center.x * this.center.x + this.center.y * this.center.y);
                this.hue = this.INIT_HUE;
                this.snows = [];
            },
            reconstructMethod: function () {
                this.render = this.render.bind(this);
            },
            createSnow: function (count, toRandomize) {
                for (var i = 0; i < count; i++) {
                    this.snows.push(new SNOW(this.width, this.height, this.center, toRandomize));
                }
            },
            render: function () {
                requestAnimationFrame(this.render);
                var gradient = this.context.createRadialGradient(this.center.x, this.center.y, 0, this.center.x, this.center.y, this.radius),
                    backgroundColor = this.BACKGROUND_COLOR.replace('%h', this.hue);
                gradient.addColorStop(0, backgroundColor.replace('%l', 30));
                gradient.addColorStop(0.2, backgroundColor.replace('%l', 20));
                gradient.addColorStop(1, backgroundColor.replace('%l', 5));
                this.context.fillStyle = gradient;
                this.context.fillRect(0, 0, this.width, this.height);
                for (var i = this.snows.length - 1; i >= 0; i--) {
                    if (!this.snows[i].render(this.context)) {
                        this.snows.splice(i, 1);
                    }
                }

                this.hue += this.DELTA_HUE;
                this.hue %= 360;
                this.createSnow(this.SNOW_COUNT.DELTA, false);
            }
        };
        var SNOW = function (width, height, center, toRandomize) {
            this.width = width;
            this.height = height;
            this.center = center;
            this.init(toRandomize);
        };
        SNOW.prototype = {
            RADIUS: 20,
            OFFSET: 4,
            INIT_POSITION_MARGIN: 20,
            COLOR: 'rgba(255, 255, 255, 0.8)',
            TOP_RADIUS: { MIN: 1, MAX: 3 },
            SCALE: { INIT: 0.04, DELTA: 0.01 },
            DELTA_ROTATE: { MIN: -Math.PI / 180 / 2, MAX: Math.PI / 180 / 2 },
            THRESHOLD_TRANSPARENCY: 0.7,
            VELOCITY: { MIN: -1, MAX: 1 },
            LINE_WIDTH: 2,
            BLUR: 10,
            init: function (toRandomize) {
                this.setParameters(toRandomize);
                this.createSnow();
            },
            setParameters: function (toRandomize) {
                if (!this.canvas) {
                    this.radius = this.RADIUS + this.TOP_RADIUS.MAX * 2 + this.LINE_WIDTH;
                    this.length = this.radius * 2;
                    this.canvas = $('<canvas />').attr({
                        width: this.length, height: this.length
                    }).get(0);
                    this.context = this.canvas.getContext('2d');
                }
                this.topRadius = this.getRandomValue(this.TOP_RADIUS);
                var theta = Math.PI * 2 * Math.random();
                this.x = this.center.x + this.INIT_POSITION_MARGIN * Math.cos(theta);
                this.y = this.center.y + this.INIT_POSITION_MARGIN * Math.sin(theta);
                this.vx = this.getRandomValue(this.VELOCITY);
                this.vy = this.getRandomValue(this.VELOCITY);
                this.deltaRotate = this.getRandomValue(this.DELTA_ROTATE);
                this.scale = this.SCALE.INIT;
                this.deltaScale = 1 + this.SCALE.DELTA * 500 / Math.max(this.width, this.height);
                this.rotate = 0;
                if (toRandomize) {
                    for (var i = 0, count = Math.random() * 1000; i < count; i++) {
                        this.x += this.vx;
                        this.y += this.vy;
                        this.scale *= this.deltaScale;
                        this.rotate += this.deltaRotate;
                    }
                }
            },
            getRandomValue: function (range) {
                return range.MIN + (range.MAX - range.MIN) * Math.random();
            },
            createSnow: function () {
                this.context.clearRect(0, 0, this.length, this.length);
                this.context.save();
                this.context.beginPath();
                this.context.translate(this.radius, this.radius);
                this.context.strokeStyle = this.COLOR;
                this.context.lineWidth = this.LINE_WIDTH;
                this.context.shadowColor = this.COLOR;
                this.context.shadowBlur = this.BLUR;
                var angle60 = Math.PI / 180 * 60,
                    sin60 = Math.sin(angle60),
                    cos60 = Math.cos(angle60),
                    threshold = Math.random() * this.RADIUS / this.OFFSET | 0,
                    rate = 0.5 + Math.random() * 0.5,
                    offsetY = this.OFFSET * Math.random() * 2,
                    offsetCount = this.RADIUS / this.OFFSET;
                for (var i = 0; i < 6; i++) {
                    this.context.save();
                    this.context.rotate(angle60 * i);
                    for (var j = 0; j <= threshold; j++) {
                        var y = -this.OFFSET * j;
                        this.context.moveTo(0, y);
                        this.context.lineTo(y * sin60, y * cos60);
                    }
                    for (var j = threshold; j < offsetCount; j++) {
                        var y = -this.OFFSET * j,
                            x = j * (offsetCount - j + 1) * rate;
                        this.context.moveTo(x, y - offsetY);
                        this.context.lineTo(0, y);
                        this.context.lineTo(-x, y - offsetY);
                    }
                    this.context.moveTo(0, 0);
                    this.context.lineTo(0, -this.RADIUS);
                    this.context.arc(0, -this.RADIUS - this.topRadius, this.topRadius, Math.PI /
                        2, Math.PI * 2.5, false);
                    this.context.restore();
                }
                this.context.stroke();
                this.context.restore();
            },
            render: function (context) {
                context.save();
                if (this.scale > this.THRESHOLD_TRANSPARENCY) {
                    context.globalAlpha = Math.max(0, (1 - this.scale) / (1 - this.THRESHOLD_TRANSPARENCY));
                    if (this.scale > 1 || this.x < -this.radius || this.x > this.width + this.radius || this.y < -this.radius || this.y > this.height + this.radius) {
                        context.restore();
                        return false;
                    }
                }
                context.translate(this.x, this.y);
                context.rotate(this.rotate);
                context.scale(this.scale, this.scale);
                context.drawImage(this.canvas, -this.radius, -this.radius);
                context.restore();
                this.x += this.vx;
                this.y += this.vy;
                this.scale *= this.deltaScale;
                this.rotate += this.deltaRotate;
                return true;
            }
        };
        $(function () {
            RENDERER.init();
        });
    </script>
</body>

</html>
Run Code Online (Sandbox Code Playgroud)

这就是我用 javafx 实现的目标:

java主类:

package fx.demo;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.stage.Stage;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CanvasTest4 extends Application {
    private static final int SNOW_COUNT_INT = 15;
    private static final int SNOW_COUNT_DELTA = 1;
    private static final String BACKGROUND_COLOR = "hsl(%h, 50%, %l%)";
    private static final double INIT_HUE = 180;
    private static final double DELTA_HUE = 0.1;

    private final double width = 1920;
    private final double height = 911;
    private final double centerX = width / 2.0;
    private final double centerY = height / 2.0;
    private final int countRate = (int) (width * height / 500 / 500);
    private final double radius = Math.sqrt(centerX * centerX + centerY * centerX);
    private double hue = INIT_HUE;
    private final List<Snow> snows = new ArrayList<>();

    private long lastUpdate;

    @Override
    public void start(Stage primaryStage) {
        Group root = new Group();
        Canvas canvas = new Canvas(width, height);
        // try to use cache
        canvas.setCache(true);
        canvas.setCacheHint(CacheHint.SPEED);
        GraphicsContext gc = canvas.getGraphicsContext2D();

        long begin = System.currentTimeMillis();
        createSnow(SNOW_COUNT_INT * countRate, width, height, centerX, centerY, true);
        System.out.printf("initial: %sms%n", (System.currentTimeMillis() - begin));
        AnimationTimer animationTimer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                System.out.printf("frame duration: %sms%n", (now - lastUpdate) / 1_000_000.0);
                lastUpdate = now;

                long start = System.currentTimeMillis();
                // draw background
                drawBackground(gc);
                System.out.printf("draw bg: %sms%n", (System.currentTimeMillis() - start));

                long l = System.currentTimeMillis();
                // draw snows
                snows.removeIf(snow -> !snow.render(gc));
                System.out.printf("draw snows: %sms%n", (System.currentTimeMillis() - l));

                // limit the number
                if (snows.size() < SNOW_COUNT_INT * countRate) {
                    createSnow(SNOW_COUNT_DELTA, width, height, centerX, centerY, false);
                }

                System.out.printf("total time: %sms%n", (System.currentTimeMillis() - start));
                System.out.println("snows: " + snows.size());
                System.out.println("-------------------------");
            }
        };
        animationTimer.start();

        root.getChildren().addAll(canvas);
        primaryStage.setScene(new Scene(root));
        primaryStage.setWidth(width);
        primaryStage.setHeight(height);
        primaryStage.show();
    }

    private void drawBackground(GraphicsContext gc) {
        String background = BACKGROUND_COLOR.replace("%h", String.valueOf(hue));
        List<Stop> stops = Arrays.asList(
                new Stop(0, Color.web(background.replace("%l", "30"))),
                new Stop(0.2, Color.web(background.replace("%l", "20"))),
                new Stop(1, Color.web(background.replace("%l", "5")))
        );
        RadialGradient radialGradient = new RadialGradient(0, 0, centerX, centerY, radius, false, CycleMethod.NO_CYCLE, stops);
        gc.setFill(radialGradient);
        gc.fillRect(0, 0, width, height);

        hue += DELTA_HUE;
        hue %= 360;
    }

    private void createSnow(int count, double width, double height, double centerX, double centerY, boolean toRandomize) {
        for (int i = 0; i < count; i++) {
            Snow snow = new Snow(width, height, centerX, centerY, toRandomize);
            snows.add(snow);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Snow 类(将在画布上渲染的形状):

package fx.demo;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.DropShadow;
import javafx.scene.paint.Color;

public class Snow {
    private static final double RADIUS = 20;
    private static final double OFFSET = 4;
    private static final double INIT_POSITION_MARGIN = 20;
    private static final Color COLOR = Color.web("rgba(255, 255, 255, 0.8)");
    private static final double TOP_RADIUS_MIN = 1;
    private static final double TOP_RADIUS_MAX = 3;
    private static final double SCALE_INIT = 0.04;
    private static final double SCALE_DELTA = 0.01;
    private static final double DELTA_ROTATE_MIN = -Math.PI / 180 / 2;
    private static final double DELTA_ROTATE_MAX = Math.PI / 180 / 2;
    private static final double THRESHOLD_TRANSPARENCY = 0.7;
    private static final double VELOCITY_MIN = -1;
    private static final double VELOCITY_MAX = 1;
    private static final double LINE_WIDTH = 2;
    private static final double BLUR = 10;

    private double length;
    private final double width;
    private final double height;
    private final double centerX;
    private final double centerY;
    private final boolean toRandomize;
    private double radius;
    private double topRadius;
    private double x;
    private double y;
    private double vx;
    private double vy;
    private double deltaRotate;
    private double scale;
    private double deltaScale;
    private double rotate;

    private double sin60;
    private double cos60;
    private double rate;
    private double offsetY;
    private double offsetCount;
    private int threshold;

    public Snow(double width, double height, double centerX, double centerY, boolean toRandomize) {
        this.width = width;
        this.height = height;
        this.centerX = centerX;
        this.centerY = centerY;
        this.toRandomize = toRandomize;

        init();
    }

    private void init() {
        this.radius = RADIUS + TOP_RADIUS_MAX * 2 + LINE_WIDTH;
        this.length = this.radius * 2;

        this.topRadius = getRandomValue(TOP_RADIUS_MIN, TOP_RADIUS_MAX);
        double theta = Math.PI * 2 * Math.random();
        this.x = centerX + INIT_POSITION_MARGIN * Math.cos(theta);
        this.y = centerY + INIT_POSITION_MARGIN * Math.sin(theta);
        this.vx = getRandomValue(VELOCITY_MIN, VELOCITY_MAX);
        this.vy = getRandomValue(VELOCITY_MIN, VELOCITY_MAX);
        this.deltaRotate = getRandomValue(DELTA_ROTATE_MIN, DELTA_ROTATE_MAX);
        this.scale = SCALE_INIT;
        this.deltaScale = 1 + SCALE_DELTA * 500 / Math.max(this.width, this.height);
        this.rotate = 0;
        double angle60 = Math.PI / 180 * 60;
        this.sin60 = Math.sin(angle60);
        this.cos60 = Math.cos(angle60);
        this.threshold = (int) (Math.random() * RADIUS / OFFSET);
        this.rate = 0.5 + Math.random() * 0.5;
        this.offsetY = OFFSET * Math.random() * 2;
        this.offsetCount = RADIUS / OFFSET;

        if (toRandomize) {
            for (int i = 0, count = (int) (Math.random() * 1000); i < count; i++) {
                this.x += this.vx;
                this.y += this.vy;
                this.scale *= this.deltaScale;
                this.rotate += this.deltaRotate;
            }
        }
    }

    public boolean render(GraphicsContext gc) {
        gc.save();
        if (this.scale > THRESHOLD_TRANSPARENCY) {
            gc.setGlobalAlpha(Math.max(0, (1 - this.scale) / (1 - THRESHOLD_TRANSPARENCY)));
            if (this.scale > 1 || this.x < -this.radius || this.x > this.width + this.radius ||
                    this.y < -this.radius || this.y > this.height + this.radius) {
                gc.restore();
                // invisible
                return false;
            }
        }

        gc.beginPath();
        gc.translate(x, y);
        gc.rotate(rotate);
        gc.scale(scale, scale);

        gc.setStroke(COLOR);
        gc.setLineWidth(LINE_WIDTH);

        DropShadow dropShadow = new DropShadow();
        dropShadow.setColor(COLOR);
        dropShadow.setRadius(BLUR);
        gc.setEffect(dropShadow);

        for (int i = 0; i < 6; i++) {
            gc.save();
            gc.rotate(60 * i);

            for (int j = 0; j <= threshold; j++) {
                double y = -4 * j;
                gc.moveTo(0, y);
                gc.lineTo(y * sin60, y * cos60);
            }
            for (int j = threshold; j < offsetCount; j++) {
                double y = -4 * j,
                        x = j * (offsetCount - j + 1) * rate;
                gc.moveTo(x, y - offsetY);
                gc.lineTo(0, y);
                gc.lineTo(-x, y - offsetY);
            }

            gc.moveTo(0, 0);
            gc.lineTo(0, -RADIUS);
            gc.arc(0, -RADIUS - this.topRadius, this.topRadius, this.topRadius, 0, 360);

            gc.restore();
        }
        gc.stroke();
        gc.restore();

        this.x += this.vx;
        this.y += this.vy;
        this.scale *= this.deltaScale;
        // origin
        this.rotate += this.deltaRotate;
        // too slowly,let it speed
        this.rotate += this.deltaRotate + this.deltaRotate > 0 ? 0.2 : -0.2;

        return true;
    }

    private double getRandomValue(double rangeMin, double rangeMax) {
        return rangeMin + (rangeMax - rangeMin) * Math.random();
    }
}
Run Code Online (Sandbox Code Playgroud)

非常感谢你的帮助。

tra*_*god 1

总结有用的评论并提供一些额外的建议,这项工作中常见的几项活动:

\n

隔离:众所周知,良好的开始是成功的一半,并且您的完整示例允许问题被单独重现和研究。

\n

目标:花一些精力确定您打算支持的最低平台功能。然后验证优化是否按预期扩大。

\n

Profile:通用分析工具可以帮助发现问题。在 JavaFX 的特定情况下,正如 @jewelsea在这里这里建议的那样,您可以启用 JavaFX 脉冲记录器。现存于 Java 8 中,在最近的版本中得到了恢复java在运行时将其启用为系统属性:

\n
-Djavafx.pulseLogger=true\n
Run Code Online (Sandbox Code Playgroud)\n

您应该看到连续编号的记录,其中包含任何花费时间超过单个脉冲的绘画的详细信息。

\n
\xe2\x80\xa6\nPULSE: 33 [16ms:20ms]\n\xe2\x80\xa6\nT16 (1 +2ms): Waiting for previous rendering\n\xe2\x80\xa6\n
Run Code Online (Sandbox Code Playgroud)\n

随着运行时优化的发展,您应该会看到过渡到显示及时绘制的记录。

\n
[34 17ms:17ms]\n[35 16ms:15ms]\n[36 16ms:13ms]\n[37 16ms:13ms]\n[38 16ms:12ms]\n[39 16ms:12ms]\n[40 16ms:12ms]\n[41 16ms:14ms]\n[42 16ms:13ms]\n
Run Code Online (Sandbox Code Playgroud)\n

时间:正如 @James_D 所观察到DropShadow“导致帧速率大幅下降。” 随着屏幕尺寸的变化,一种策略是让画布增大以填充封闭的父级,如此处所述。通过这种方式,渲染负担可以适应环境或由用户调整。下面的变体使用此处所示的基本方法,而此方法说明了自定义布局。此外,该示例省略了DropShadow任意以下的渲染scale

\n
if (scale > 0.20) gc.setEffect(dropShadow);\n
Run Code Online (Sandbox Code Playgroud)\n

内存:由于DropShadow实例保持不变,因此可以仅实例化一次并在render(). 该示例也用于RADIUS实例。

\n

代码

\n
import javafx.animation.AnimationTimer;\nimport javafx.application.Application;\nimport javafx.scene.CacheHint;\nimport javafx.scene.Scene;\nimport javafx.scene.canvas.Canvas;\nimport javafx.scene.canvas.GraphicsContext;\nimport javafx.scene.paint.Color;\nimport javafx.scene.paint.CycleMethod;\nimport javafx.scene.paint.RadialGradient;\nimport javafx.scene.paint.Stop;\nimport javafx.stage.Stage;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport javafx.scene.control.Label;\nimport javafx.scene.effect.DropShadow;\nimport javafx.scene.layout.BorderPane;\nimport javafx.scene.layout.Pane;\n\n/**\n * @see /sf/ask/5452353091/\n */\npublic class CanvasSnow extends Application {\n\n    private static final double PREF_WIDTH = 1200;\n    private static final double PREF_HEIGHT = PREF_WIDTH * 0.618;\n    private static final int SNOW_COUNT_INT = 15;\n    private static final int SNOW_COUNT_DELTA = 1;\n    private static final String BACKGROUND_COLOR = "hsl(%h, 50%, %l%)";\n    private static final double INIT_HUE = 180;\n    private static final double DELTA_HUE = 0.1;\n    private final List<Snow> snows = new ArrayList<>();\n    private double hue = INIT_HUE;\n    private long lastUpdate;\n\n    @Override\n    public void start(Stage primaryStage) {\n        var root = new BorderPane();\n        var status = new Label("Rate Hz.");\n        var canvas = new Canvas(PREF_WIDTH, PREF_HEIGHT);\n        canvas.setCache(true);\n        canvas.setCacheHint(CacheHint.SPEED);\n        var view = new Pane(canvas);\n        view.setPrefSize(PREF_WIDTH, PREF_HEIGHT);\n        var gc = canvas.getGraphicsContext2D();\n        var countRate = (int) (canvas.getWidth() * canvas.getHeight() / 500 / 500);\n        createSnow(canvas, SNOW_COUNT_INT * countRate, true);\n        AnimationTimer animationTimer = new AnimationTimer() {\n            @Override\n            public void handle(long now) {\n                status.setText(String.format("Snows: %d; Rate: %7.4f Hz.",\n                    snows.size(), 1.0e9 / (now - lastUpdate)));\n                lastUpdate = now;\n                drawBackground(canvas);\n                snows.removeIf(snow -> !snow.render(gc));\n                if (snows.size() < SNOW_COUNT_INT * countRate) {\n                    createSnow(canvas, SNOW_COUNT_DELTA, false);\n                }\n            }\n        };\n        canvas.widthProperty().bind(view.widthProperty());\n        canvas.heightProperty().bind(view.heightProperty());\n        root.setCenter(view);\n        root.setBottom(status);\n        primaryStage.setScene(new Scene(root));\n        primaryStage.show();\n        animationTimer.start();\n    }\n\n    private void drawBackground(Canvas canvas) {\n        String background = BACKGROUND_COLOR.replace("%h", String.valueOf(hue));\n        List<Stop> stops = Arrays.asList(\n            new Stop(0, Color.web(background.replace("%l", "30"))),\n            new Stop(0.2, Color.web(background.replace("%l", "20"))),\n            new Stop(1, Color.web(background.replace("%l", "5")))\n        );\n        var gc = canvas.getGraphicsContext2D();\n        var centerX = canvas.getWidth() / 2;\n        var centerY = canvas.getHeight() / 2;\n        var radius = Math.sqrt(centerX * centerX + centerY * centerX);\n        RadialGradient radialGradient = new RadialGradient(0, 0, centerX, centerY, radius, false, CycleMethod.NO_CYCLE, stops);\n        gc.setFill(radialGradient);\n        gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());\n        hue += DELTA_HUE;\n        hue %= 360;\n    }\n\n    private void createSnow(Canvas canvas, int count, boolean toRandomize) {\n        var gc = canvas.getGraphicsContext2D();\n        var width = canvas.getWidth();\n        var height = canvas.getHeight();\n        var centerX = canvas.getWidth() / 2;\n        var centerY = canvas.getHeight() / 2;\n        for (int i = 0; i < count; i++) {\n            Snow snow = new Snow(width, height, centerX, centerY, toRandomize);\n            snows.add(snow);\n        }\n    }\n\n    private static class Snow {\n\n        private static final double RADIUS = 20;\n        private static final double OFFSET = 4;\n        private static final double INIT_POSITION_MARGIN = 20;\n        private static final Color COLOR = Color.web("rgba(255, 255, 255, 0.8)");\n        private static final double TOP_RADIUS_MIN = 1;\n        private static final double TOP_RADIUS_MAX = 3;\n        private static final double SCALE_INIT = 0.04;\n        private static final double SCALE_DELTA = 0.01;\n        private static final double DELTA_ROTATE_MIN = -Math.PI / 180 / 2;\n        private static final double DELTA_ROTATE_MAX = Math.PI / 180 / 2;\n        private static final double THRESHOLD_TRANSPARENCY = 0.7;\n        private static final double VELOCITY_MIN = -1;\n        private static final double VELOCITY_MAX = 1;\n        private static final double LINE_WIDTH = 2;\n        private final DropShadow dropShadow = new DropShadow(RADIUS, COLOR);\n        private final double width;\n        private final double height;\n        private final double centerX;\n        private final double centerY;\n        private final boolean toRandomize;\n        private double radius;\n        private double topRadius;\n        private double x;\n        private double y;\n        private double vx;\n        private double vy;\n        private double deltaRotate;\n        private double scale;\n        private double deltaScale;\n        private double rotate;\n        private double sin60;\n        private double cos60;\n        private double rate;\n        private double offsetY;\n        private double offsetCount;\n        private int threshold;\n\n        public Snow(double width, double height, double centerX, double centerY, boolean toRandomize) {\n            this.width = width;\n            this.height = height;\n            this.centerX = centerX;\n            this.centerY = centerY;\n            this.toRandomize = toRandomize;\n            init();\n        }\n\n        private void init() {\n            this.radius = RADIUS + TOP_RADIUS_MAX * 2 + LINE_WIDTH;\n            this.topRadius = getRandomValue(TOP_RADIUS_MIN, TOP_RADIUS_MAX);\n            double theta = Math.PI * 2 * Math.random();\n            this.x = centerX + INIT_POSITION_MARGIN * Math.cos(theta);\n            this.y = centerY + INIT_POSITION_MARGIN * Math.sin(theta);\n            this.vx = getRandomValue(VELOCITY_MIN, VELOCITY_MAX);\n            this.vy = getRandomValue(VELOCITY_MIN, VELOCITY_MAX);\n            this.deltaRotate = getRandomValue(DELTA_ROTATE_MIN, DELTA_ROTATE_MAX);\n            this.scale = SCALE_INIT;\n            this.deltaScale = 1 + SCALE_DELTA * 500 / Math.max(this.width, this.height);\n            this.rotate = 0;\n            double angle60 = Math.PI / 180 * 60;\n            this.sin60 = Math.sin(angle60);\n            this.cos60 = Math.cos(angle60);\n            this.threshold = (int) (Math.random() * RADIUS / OFFSET);\n            this.rate = 0.5 + Math.random() * 0.5;\n            this.offsetY = OFFSET * Math.random() * 2;\n            this.offsetCount = RADIUS / OFFSET;\n            if (toRandomize) {\n                for (int i = 0, count = (int) (Math.random() * 1000); i < count; i++) {\n                    this.x += this.vx;\n                    this.y += this.vy;\n                    this.scale *= this.deltaScale;\n                    this.rotate += this.deltaRotate;\n                }\n            }\n        }\n\n        public boolean render(GraphicsContext gc) {\n            gc.save();\n            if (this.scale > THRESHOLD_TRANSPARENCY) {\n                gc.setGlobalAlpha(Math.max(0, (1 - this.scale) / (1 - THRESHOLD_TRANSPARENCY)));\n                if (this.scale > 1 || this.x < -this.radius || this.x > this.width + this.radius\n                    || this.y < -this.radius || this.y > this.height + this.radius) {\n                    gc.restore();\n                    // invisible\n                    return false;\n                }\n            }\n            gc.beginPath();\n            gc.translate(x, y);\n            gc.rotate(rotate);\n            gc.scale(scale, scale);\n            gc.setStroke(COLOR);\n            gc.setLineWidth(LINE_WIDTH);\n            if (scale > 0.20) gc.setEffect(dropShadow);\n            for (int i = 0; i < 6; i++) {\n                gc.save();\n                gc.rotate(60 * i);\n                for (int j = 0; j <= threshold; j++) {\n                    double y = -4 * j;\n                    gc.moveTo(0, y);\n                    gc.lineTo(y * sin60, y * cos60);\n                }\n                for (int j = threshold; j < offsetCount; j++) {\n                    double y = -4 * j,\n                        x = j * (offsetCount - j + 1) * rate;\n                    gc.moveTo(x, y - offsetY);\n                    gc.lineTo(0, y);\n                    gc.lineTo(-x, y - offsetY);\n                }\n                gc.moveTo(0, 0);\n                gc.lineTo(0, -RADIUS);\n                gc.arc(0, -RADIUS - this.topRadius, this.topRadius, this.topRadius, 0, 360);\n                gc.restore();\n            }\n            gc.stroke();\n            gc.restore();\n            this.x += this.vx;\n            this.y += this.vy;\n            this.scale *= this.deltaScale;\n            // origin\n            this.rotate += this.deltaRotate;\n            // too slowly,let it speed\n            this.rotate += this.deltaRotate + this.deltaRotate > 0 ? 0.2 : -0.2;\n            return true;\n        }\n\n        private double getRandomValue(double rangeMin, double rangeMax) {\n            return rangeMin + (rangeMax - rangeMin) * Math.random();\n        }\n    }\n\n    public static void main(String[] args) {\n        Application.launch();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n