Merry Christmas by JavaFX

This entry is the translation of 25 Dec. entry of JavaFX Advent Calendar.

The previous entry was Java FX on PI so far ‘beta’ to use by @masafumi_ohta.

The original entry was published on Christmas, so I wrote Christmas Card application by JavaFX.

The image of accomplished application is shown below:

I wrote two animations: one is snow flying, and the other is neon illumination .

Background image is ImageView. I used Canvas class for snow flying and shapes for llumination.

Snow flake is just circle, drawn by GraphicsContext#fillOval method. To add blur, GraphicsContext#setEffect method was used.

The snow radius, transparency, and size of blur is decided by random value.

    private double x;
    private double y;
    private double radius;
    private Color color;
    private BoxBlur blur;
    
    private Random random = new Random();
    
    public SnowParticle(double initX) {
        x = initX;
        y = 0.0;
        radius = random.nextDouble() * MAX_RADIUS;
        color = Color.rgb(255, 255, 255, random.nextDouble());
        double blurSize = random.nextDouble() * MAX_BLUR + MAX_BLUR;  
        blur = new BoxBlur(blurSize, blurSize, 2);
    }
    
    public void draw(GraphicsContext context) {
        context.setFill(color);
        context.setEffect(blur);
        
        context.fillOval(x, y, radius, radius);
    }

One of constructor argument, initX is initial x position of snow. Because every snow flies from y = 0, y is not assigned.

Because of random determination of radius, transparency and size of blur, appearances of snow flakes vary. This shows a sense of perspective.

I didn't use Timeline class nor Transition, but AnimationTimer class.

AnimationTimer calls handle method in appropriate cycle. Therefore, the coordinate of snow is updated by random value and drawn.

The coordinate updating process is described in SnowParticle#update method.

    public double update() {
        x += random.nextDouble() * MAX_STAGGER - MAX_STAGGER / 2.0;
        y += random.nextDouble() * MAX_STAGGER;
        
        return y;
    }

To snow lightly, the next coordinate of snow is defined by random. As well as y-coordinate, x-coordinate is defined by random.

The method that calls update method and draw method of SnowParticle class is update method of ChristmasCard class.

There are many snow flakes in the scene, so List object store the SnowParticle objects. If y-coordinate of SnowParticle object is bigger than height of Scene, the SnowParticle object is removed from List. When calling ChristmasCard#update method, SnowParticle object is created randomly.

When drawing to Canvas, we should paint the Canvas with transparency color at first, then draw snow.

    private void update() {
        GraphicsContext context = snowCanvas.getGraphicsContext2D();
        
        context.setFill(Color.rgb(0, 0, 0, 0.0));
        context.fillRect(0, 0, snowCanvas.getWidth(), snowCanvas.getHeight());

        Iterator<SnowParticle> it = particles.iterator();
        while (it.hasNext()) {
            SnowParticle particle = it.next();
            
            double y = particle.update();
            if (y >= snowCanvas.getHeight()) {
                // When SnowParticle move to the bottom of Scene
                // it is removed
                it.remove();
            } else {       
                particle.draw(context);
            }
        }
        
        // Creating SnowParticle object randomly
        if (random.nextInt(3) == 0) {
            particles.add(new SnowParticle(random.nextDouble() * snowCanvas.getWidth()));
        }
    }

Next is the illumination. I thought I used Text class for illumination at first. But I used SVG.

I used SVGLoader for loading SVG file.

Shape objects loaded from SVG file are divided into stroke and fill. Strokes and fills are animated separately.

There are 14 shapes (14 characters), so I used for loop to set animation. I didn't know that I wasn't able to add KeyValue objects to KeyFrame object after KeyFrame instanciation, because Set object gotten by KeyFrame#getValues was immutable.

Therefore, I make KeyFrame object in the loop, and then add the KeyFrame object to Timeline object.

    private void initIllumination() {
        Timeline timeline = new Timeline();
        
        SVGContent svgContent = SVGLoader.load(getClass().getResource("illumination.svg").toString());
        
        for (int i = 1; i < 15; i++ ) {
            Shape ch = (Shape)svgContent.getNode(String.format("merry%02d", i));
            ch.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.YELLOW, 20.0, 0.4, 0.0, 0.0));
            ch.setStroke(TRANSPARENT);
            ch.setFill(TRANSPARENT);
            ch.setTranslateX(50);
            ch.setTranslateY(40);

            illuminationPane.getChildren().add(ch);

            KeyFrame frame0 = new KeyFrame(Duration.ZERO,
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame1 = new KeyFrame(Duration.seconds(2),
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame2 = new KeyFrame(Duration.seconds(5),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame3 = new KeyFrame(Duration.seconds(6),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), Color.WHITE));
            KeyFrame frame4 = new KeyFrame(Duration.seconds(10),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), Color.WHITE));
            KeyFrame frame5 = new KeyFrame(Duration.seconds(12),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame6 = new KeyFrame(Duration.seconds(15),
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));

            timeline.getKeyFrames().addAll(frame0, frame1, frame2, frame3, frame4, frame5, frame6);
        }
        
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();
    }

To shine characters, I used DropShadow. Bloom is also OK, but I prefer DropShadow class because DropShadow has properties for appearance adjustment.

I uploaded this project to GitHub: JavaFXChristmasCard.