I'm using Javafx to achieve the same effect in an HTML5 demo, but with Javafx the frame rate is very low
how do I optimize it, am I writing it incorrectly or is there another better way to achieve it.
this is the original html5 demo project:
<!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>
and then this is what I achieved with javafx:
the java main class:
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);
}
}
}
the Snow class (the shaps which will render on canvas):
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();
}
}
Thank you so much for your help.
Summarizing the helpful comments and offering a few additional suggestions, several activities are common in this effort:
Isolate: Proverbially, well begun is half done, and your complete example allows the problem to be reproduced and studied in isolation.
Target: Devote some attention to identifying minimum platform capabilities that you intend to support. Then verify that optimizations scale up as desired.
Profile: General profiling tools can help spot problems. In the particular case of JavaFX, as @jewelsea suggests here and here, you can enable the JavaFX Pulse Logger. Extant in Java 8, it was restored in recent versions. Enable it as a
javasystem property at run-time:You should see consecutively numbered records with details about any painting that took longer than a single pulse.
As run-time optimization evolves, you should see a transition to records showing timely painting.
Time: As @James_D observes here,
DropShadow"is causing a huge decrease in the frame rate." As screen sizes vary, one strategy is to let the canvas grow to fill the enclosing parent, as discussed here. In this way the rendering burden can be adapted to the environment or by the user. The variation below uses the basic approach shown here, while this approach illustrates a custom layout. In addition, the example omitsDropShadowrendering below an arbitraryscale:Memory: As the
DropShadowinstance remains constant, it can be instantiated just once and used repeatedly inrender(). The example also usesRADIUSfor the instance.Code: