JavaFX Invaders game

Hi all

My first contribution to this site:
JDK: OpenJDK11 + JavaFX11

Feel free to feedback :smiley:

Everything in one file


import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;

/**
 *
 * @author oldhandmixer
 * 
 * This version contains basic vector operations and a simple particle demo
 * 
 */
public class Particles extends Application {

    final double height = 1080;
    final double width = 1920;
    AnimationTimer timer;
    final Canvas canvas = new Canvas(1920, 1080);
    PParticle ppp[] = new PParticle[200];
    double mousex, mousey;

    @Override

    public void start(Stage stage) {
        Group root = new Group();
        Scene s = new Scene(root, width, height, Color.BLACK);
        for (int i = 0; i < ppp.length; i++) {
            ppp[i] = new PParticle();
            ppp[i].col = Color.rgb((int)(255 * Math.random()), (int)(255 * Math.random()), (int)(255 * Math.random()));
        }
        s.setOnMouseMoved(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent e) {
                mousex = e.getSceneX();
                mousey = e.getSceneY();
            }
        });

        root.getChildren().add(canvas);
        stage.setScene(s);
        stage.show();
        gameloop();
    }

    void gameloop() {
        timer = new AnimationTimer() {
            @Override
            public void handle(long l) {
                GraphicsContext gc = canvas.getGraphicsContext2D();
//                gc.clearRect(0, 0, 1920, 1080);
                gc.setFill(Color.rgb(0,0,0,0.050));
                gc.fillRect(0, 0, width, height);
                gc.setLineCap(StrokeLineCap.ROUND);
                gc.setLineWidth(15.0);
                for (int i = 0; i < ppp.length; i++) {
                    gc.beginPath();
                    ppp[i].update();
                    ppp[i].checkEdges();
                    ppp[i].display(gc);
                    gc.stroke();
                    gc.closePath();
                }
            }
        };
        timer.start();
    }

    public static void main(String[] args) {
        launch();
    }

    /**
     * Particle class
     */
    class PParticle {

        PVector loc;
        PVector vel;
        PVector acc;
        Color col;
        double topspeed = 10;

        public PParticle() {
            loc = new PVector(width / 2, height / 2);
            vel = new PVector(5 * Math.random(), 5 * Math.random());
        }

        void update() {
            PVector mouse = new PVector(mousex, mousey);
            PVector dir = PVector.sub(mouse, loc);
            dir.normalize();
            dir.mult(Math.random()*0.5);
            acc = dir;
            vel.add(acc);
            vel.limit(topspeed);
            loc.add(vel);
        }

        void checkEdges() {
            if (loc.x > width) {
                loc.x = 0;
            } else if (loc.x < 0) {
                loc.x = width;
            }

            if (loc.y > height) {
                loc.y = 0;
            } else if (loc.y < 0) {
                loc.y = height;
            }
        }

        void display(GraphicsContext gc) {
//            gc.setFill(Color.RED);
//            gc.fillRect(loc.x, loc.y, 5, 5);
              gc.setStroke(col);
              gc.moveTo(loc.x, loc.y);
              gc.lineTo(loc.x, loc.y);
        }

    }

}

/**
 * Vector class to help making the particles simpler
 */
class PVector {

    double x;
    double y;

    PVector(double x_, double y_) {
        x = x_;
        y = y_;
    }

    void add(PVector v) {
        x += v.x;
        y += v.y;
    }

    static PVector add(PVector v, PVector u) {
        return (new PVector(v.x + u.x, v.y + u.y));
    }

    void sub(PVector v) {
        x -= v.x;
        y -= v.y;
    }

    static PVector sub(PVector v, PVector u) {
        return (new PVector(v.x - u.x, v.y - u.y));
    }

    static PVector random2D() {
        double angle = 2 * Math.PI * Math.random();
        return (new PVector(Math.cos(angle), Math.sin(angle)));
    }

    void mult(double n) {
        x *= n;
        y *= n;
    }

    void div(double n) {
        x /= n;
        y /= n;
    }

    double mag() {
        return (double) Math.sqrt(x * x + y * y);
    }

    void normalize() {
        double m = mag();
        if (m != 0) {
            div(m);
        }
    }

    void limit(double max) {
        if (mag() > max) {
            normalize();
            mult(max);
        }
    }
}

Edited to correct my 1337 HAXOR mistakes ::slight_smile:

Here is the real thing. Still one big file



import java.util.ArrayList;
import java.util.List;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 *
 * @author oldhandmixer
 */
public class Invaders extends Application {

    int levels[][] = {
        // Level 0
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        // Level 1
        {0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0},
        {1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1},
        {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1},
        {1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1},
        {1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1},
        {1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1},
        {0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0},};

    final double height = 1080;
    final double width = 1920;
    AnimationTimer timer;
    final Canvas canvas = new Canvas(width, height);
    PGoodBoi lene = new PGoodBoi(width / 2, height - 80);
    List<PBadBoi> baddies = new ArrayList<>();
    List<PDrops> missiles = new ArrayList<>();
    List<PBomb> bombs = new ArrayList<>();
    double mousex, mousey;
    boolean kdown = false;
    boolean kup = false;
    boolean kleft = false;
    boolean krigth = false;

    @Override

    public void start(Stage stage) {
        Group root = new Group();
        Scene s = new Scene(root, width, height, Color.BLACK);
        lene.loc = new PVector(width / 2, height - 80);

        s.setOnMouseMoved(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent e) {
                mousex = e.getSceneX();
                mousey = e.getSceneY();
            }
        });

        s.setOnMousePressed(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent e) {
                PDrops p = new PDrops(lene.loc, lene.d, 3);
                missiles.add(p);
            }
        });

        s.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent e) {
                switch (e.getCode()) {
                    case A:
                        kleft = true;
                        break;
                    case D:
                        krigth = true;
                        break;
                    case W:
                        kup = true;
                        break;
                    case S:
                        kdown = true;
                        break;
                }
            }
        });

        s.setOnKeyReleased(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent e) {
                switch (e.getCode()) {
                    case A:
                        kleft = false;
                        break;
                    case D:
                        krigth = false;
                        break;
                    case W:
                        kup = false;
                        break;
                    case S:
                        kdown = false;
                        break;
                }
            }
        });

        root.getChildren().add(canvas);
        s.setCursor(Cursor.NONE);
        stage.setScene(s);
        stage.show();
        initInvaders(1);
        gameloop();
    }

    public void initInvaders(int level) {
        int p1 = level * 8;
        baddies.clear();
        for (int i = 0; i < 8; i++) {
            int[] m = levels[p1 + i];
            int y = 2 + i * 18 * 5;
            for (int j = 0; j < m.length; j++) {
                if (m[j] > 0) {
                    int x = 4 + j * 18 * 5;
                    PBadBoi p = new PBadBoi(x, y);
                    baddies.add(p);
                }
            }
        }
    }

    void gameloop() {

        timer = new AnimationTimer() {
            double tick = 0;
            int state = 0; // 0:rigth -> 1: down -> 2:left -> 3: down
            double baddiespeed = 1;
            int downcount = 0;

            @Override
            public void handle(long l) {
                double xxx = 0;
                double yyy = 0;
                GraphicsContext gc = canvas.getGraphicsContext2D();
                PVector mloc = new PVector(mousex, mousey);
                gc.clearRect(0, 0, width, height);
                
                // update the good guy
                lene.d = PVector.sub(mloc, lene.loc);
                lene.d.normalize();
                lene.update();
                lene.checkEdges();

                // move the baddies
                switch (state) {
                    case 0: // move baddies to the rigth
                        boolean hitTheEdge = false;
                        PVector vel = new PVector(1, 0);
                        vel.mult(baddiespeed);
                        for (int i = 0; i < baddies.size(); i++) {
                            baddies.get(i).loc.add(vel);
                            if (baddies.get(i).loc.x > 1850) {
                                hitTheEdge = true;
                            }
                        }
                        if (hitTheEdge) {
                            state++; // next frame will be state 1
                            downcount = 10; // next state will be repeated <downcount> times
                        }
                        break;
                    case 1: // move baddies down at the rigth side of the screen
                        vel = new PVector(0, 1);
                        vel.mult(baddiespeed);
                        for (int i = 0; i < baddies.size(); i++) {
                            baddies.get(i).loc.add(vel);
                        }
                        if (downcount-- == 0) {
                            state++; // next frame will be state 2
                        }
                        break;
                    case 2: // move baddies to the left
                        hitTheEdge = false;
                        vel = new PVector(-1, 0);
                        vel.mult(baddiespeed);
                        for (int i = 0; i < baddies.size(); i++) {
                            baddies.get(i).loc.add(vel);
                            if (baddies.get(i).loc.x < 20) {
                                hitTheEdge = true;
                            }
                        }
                        if (hitTheEdge) {
                            state++; // next frame will be state 3
                            downcount = 10; // next state will be repeated <downcount> times
                        }
                        break;
                    case 3: // move baddies down at the left side of the screen
                        vel = new PVector(0, 1);
                        vel.mult(baddiespeed);
                        for (int i = 0; i < baddies.size(); i++) {
                            baddies.get(i).loc.add(vel);
                        }
                        if (downcount-- == 0) {
                            state = 0; // next frame will be state 0
                        }
                        break;
                }

                // collission detect missiles
                for (int i = 0; i < missiles.size(); i++) {
                    missiles.get(i).update();
                    if (!missiles.get(i).checkEdges()) {
                        missiles.remove(i--);
                    } else {
                        // collision detect missiles against baddies
                        boolean hit = false;
                        for (int j = 0; j < baddies.size(); j++) {
                            hit = missiles.get(i).checkCollision(baddies.get(j));
                            if (hit) {
                                missiles.remove(i--);
                                baddies.remove(j);
                                break;
                            }
                        }
                        if (!hit) {
                            missiles.get(i).display(gc);
                        }
                    }
                }

                // render the bad guys
                tick += Math.PI / 30;
                tick = (tick > Math.PI * 2) ? tick - 2 * Math.PI : tick;
                baddies.forEach((p) -> {
                    p.display(tick, gc);
                });
                //Do the bombing
                if (Math.random() < 0.05) {
                    int i = (int) (Math.random() * baddies.size());
                    bombs.add(new PBomb(baddies.get(i).loc));
                }
                // collision detect goodboi against bombs
                for (int i = 0; i < bombs.size(); i++) {
                    bombs.get(i).update();
                    if (!bombs.get(i).checkEdges()) {
                        bombs.remove(i--);
                    } else {
                        double hit = bombs.get(i).loc.dist(lene.loc);
                        if (hit < 23) {
                            bombs.remove(i--);
                            // @todo die lene here
                        } else {
                            bombs.get(i).display(gc);
                        }
                    }
                }

                // render crosshair
                gc.setFill(Color.YELLOW);
                double s = 3;
                gc.fillRect(mloc.x - 8 * s, mloc.y - 1 * s, 6 * s, 2 * s);
                gc.fillRect(mloc.x + 2 * s, mloc.y - 1 * s, 6 * s, 2 * s);
                gc.fillRect(mloc.x - 1 * s, mloc.y - 8 * s, 2 * s, 6 * s);
                gc.fillRect(mloc.x - 1 * s, mloc.y + 2 * s, 2 * s, 6 * s);

                // render good boi
                lene.display(gc);
            }
        };
        timer.start();
    }

    public static void main(String[] args) {
        launch();
    }

    class PBomb {

        PVector d;
        PVector loc;
        PVector vel;
        private static final int s = 30;
        private static final int w = 5;

        public PBomb(PVector l) {
            loc = new PVector(l.x + 30, l.y);
            d = new PVector(0, 1);
            vel = new PVector(d.x, d.y);
            vel.normalize();
            vel.mult(2 + 3 * Math.random());
        }

        void update() {
            loc.add(vel);
        }

        // returns false if drop is outside edges
        boolean checkEdges() {
            boolean ret = true;
            if (loc.x > width) {
                ret = false;
            } else if (loc.x < 0) {
                ret = false;
            }

            if (loc.y > height) {
                ret = false;
            } else if (loc.y < 0) {
                ret = false;
            }
            return ret;
        }

        // returns true if there is a collision
        boolean checkCollision(PGoodBoi p) {
            boolean ret = false;

            if ((loc.x > p.loc.x) && (loc.x < p.loc.x + 50)
                    && (loc.y > p.loc.y) && (loc.y < p.loc.y + 65)) {
                ret = true;
            }

            return ret;
        }

        void display(GraphicsContext gc) {
            gc.setFill(Color.YELLOW);
            gc.fillOval(loc.x - 5, loc.y - 5, 10, 10);
        }
    }

    class PDrops {

        PVector d;
        PVector loc;
        PVector vel;
        private static final int s = 30;
        private static final int w = 5;

        public PDrops(PVector l, PVector d_, double s_) {
            loc = new PVector(l.x, l.y);
            d = new PVector(d_.x, d_.y);
            vel = new PVector(d.x, d.y);
            vel.normalize();
            vel.mult(s_);
        }

        void update() {
            loc.add(vel);
        }

        // returns false if drop is outside edges
        boolean checkEdges() {
            boolean ret = true;
            if (loc.x > width) {
                ret = false;
            } else if (loc.x < 0) {
                ret = false;
            }

            if (loc.y > height) {
                ret = false;
            } else if (loc.y < 0) {
                ret = false;
            }
            return ret;
        }

        // returns true if there is a collision
        boolean checkCollision(PBadBoi p) {
            boolean ret = false;

            if ((loc.x > p.loc.x) && (loc.x < p.loc.x + 50)
                    && (loc.y > p.loc.y) && (loc.y < p.loc.y + 65)) {
                ret = true;
            } else if ((loc.x + d.x * s > p.loc.x) && (loc.x + d.x * s < p.loc.x + 50)
                    && (loc.y + d.y * s > p.loc.y) && (loc.y + d.y * s < p.loc.y + 65)) {
                ret = true;
            }

            return ret;
        }

        void display(GraphicsContext gc) {
            gc.beginPath();
            gc.setStroke(Color.YELLOW);
            gc.setLineWidth(w);
            gc.moveTo(loc.x, loc.y);
            gc.lineTo(loc.x + s * d.x, loc.y + s * d.y);
            gc.stroke();
            gc.closePath();
//            gc.fillRect(loc.x, loc.y, 20, 20);
        }
    }

    class PGoodBoi {

        PVector loc;
        PVector vel;
        PVector acc;
        PVector d;
        double topspeed = 10;
        private static final int s = 3;

        public PGoodBoi(double x, double y) {
            loc = new PVector(x, y);
            acc = new PVector(0, 0);
            vel = new PVector(0, 0);
        }

        void update() {
            if (kup) {
                acc.x = d.x;
                acc.y = d.y;
                acc.mult(0.05);
                vel.add(acc);
                if (vel.mag() > topspeed) {
                    vel.normalize();
                    vel.mult(topspeed);
                }
            }
            loc.add(vel);
        }
        
        void checkEdges(){
            if ((loc.x < 5)||(loc.x > 1880)){
                vel.x *= -1;
            }
            if ((loc.y< 5)||(loc.y > 1070)) {
                vel.y *= -1;
            }
        }

        void display(GraphicsContext gc) {
            double angle = (Math.asin(d.y) < 0) ? Math.PI / 2 - Math.acos(d.x) : Math.PI / 2 + Math.acos(d.x);
            angle = 360 * angle / (2 * Math.PI);
            gc.translate(lene.loc.x, lene.loc.y);
            gc.rotate(angle);
            double xxx = - 9 * s;
            double yyy = - 3 * s;
            gc.setFill(Color.YELLOW);
            gc.fillRect(xxx + 8 * s, yyy, 3 * s, 3 * s);
            gc.fillRect(xxx + 7 * s, yyy + 3 * s, 5 * s, 1 * s);
            gc.fillRect(xxx + 1 * s, yyy + 4 * s, 16 * s, 1 * s);
            gc.fillRect(xxx, yyy + 5 * s, 18 * s, 5 * s);
            gc.rotate(-angle);
            gc.translate(-lene.loc.x, -lene.loc.y);
        }
    }

    class PBadBoi {

        PVector loc;

        private static final int dd = 5;

        public PBadBoi(double x, double y) {
            loc = new PVector(x, y);
        }

        void display(double tick, GraphicsContext gc) {
            gc.setFill(Color.RED);
            gc.fillRect(loc.x + 1 * dd, loc.y, 8 * dd, 1 * dd);
            gc.fillRect(loc.x, loc.y + 1 * dd, 10 * dd, 9 * dd);
            var xx = loc.x + 3 * Math.sin(tick) * dd;
            gc.fillRect(xx, loc.y + 8 * dd, 4 * dd, 5 * dd);
            gc.fillRect(xx + 6 * dd, loc.y + 8 * dd, 4 * dd, 5 * dd);
            gc.setFill(Color.WHITE);
            gc.fillRect(loc.x + 2 * dd, loc.y + 2 * dd, 2 * dd, 4 * dd);
            gc.fillRect(loc.x + 6 * dd, loc.y + 2 * dd, 2 * dd, 4 * dd);
            gc.setFill(Color.BLACK);
            gc.fillRect(loc.x + 2 * dd, loc.y + 4 * dd, 1 * dd, 2 * dd);
            gc.fillRect(loc.x + 6 * dd, loc.y + 4 * dd, 1 * dd, 2 * dd);
        }
    }
}

/**
 * Vector class to help making the particles simpler
 */
class PVector {

    double x;
    double y;

    PVector(double x_, double y_) {
        x = x_;
        y = y_;
    }

    void add(PVector v) {
        x += v.x;
        y += v.y;
    }

    static PVector add(PVector v, PVector u) {
        return (new PVector(v.x + u.x, v.y + u.y));
    }

    void sub(PVector v) {
        x -= v.x;
        y -= v.y;
    }

    static PVector sub(PVector v, PVector u) {
        return (new PVector(v.x - u.x, v.y - u.y));
    }

    static PVector random2D() {
        double angle = 2 * Math.PI * Math.random();
        return (new PVector(Math.cos(angle), Math.sin(angle)));
    }

    void mult(double n) {
        x *= n;
        y *= n;
    }

    void div(double n) {
        x /= n;
        y /= n;
    }

    double mag() {
        return (double) Math.sqrt(x * x + y * y);
    }

    double dist(PVector p) {
        double dx = x - p.x;
        double dy = y - p.y;
        return (double) Math.sqrt(dx * dx + dy * dy);
    }

    void normalize() {
        double m = mag();
        if (m != 0) {
            div(m);
        }
    }

    void limit(double max) {
        if (mag() > max) {
            normalize();
            mult(max);
        }
    }
}


I noticed, I just didn’t say anything to see if anyone else did :stuck_out_tongue:

[quote]JavaFX Invaders game (Read 109 times)
[/quote]
:point: :persecutioncomplex: ::slight_smile:

Cas :slight_smile:

If you want people to try it and give feedback you need to provide an easy one-click-download with either a game.jar or a zipped folder with run_game.bat/.sh and the game files. Most people who haven’t already aren’t going to set up their JavaFX SDK just to try out your little demo unless it’s pretty awesome.

I switched to OpenJDK 11 on Windows some time ago and my JavaFX install is somehow borked:

Error: JavaFX runtime components are missing, and are required to run this application

With the removal of JavaFX from the JDK the whole situation got a bit messed up IMHO

The code looks nice and clean tho so i gave an upvote anyway :point:

@hekras do you ever use the ‘this’ keyword? or do you just pretend that it doesn’t exist

Yes, creating a .jar with Java 11 and JavaFX is kind of involved. And even after installing both, it also requires a rather more complicated console line than the classic “java -jar programName.jar”. Also, the locations of the java and javafx modules and runnables are not necessarily going to be the same for everyone’s install, so writing a .bat or .sh is problematic.

A possible solution is to package this as a jlink structure. But even there, one has to do this separately for Windows and Mac.

I tried to copy and run the code (I have the 11-split handled) but when I did the paste, the line numbers came over making the code unrunnable.

Why am I supposed to know all the modules my application needs and type them into the command line? In other words: What’s the best way to automate this (e.g. with IDE or without).

Select the text starting at the first letter of the code, i.e. the “i” of import :point:

Cheers

Thanks helping out here

Jar / Zip

  • I did actually read the thread with how to pack and deploy and I will supply this thread with a 1-file package

this.
Yes, my coding style… hmmm, I rarely use this,
also I use inner classes whenever I can. I will only declare a class outside the main class if I absolutely have to.
To me a 10.000+ line sourcefile is preferred before a 2+ files

Forum help
I did look for help about how to post pictures and I assume linking to files is alsmost the same, but could someone please hel me out here
I did find out how to upload
Also, how do I add a picture to the profile, when I use my google account for sign in

Just a small note but not many people realise you can actually declare more than one class at the top level of a .java source file.

Cas :slight_smile:

Doh ::slight_smile: Thanks, @Shatterhand, for the reminder about how to copy/paste.

I got the Invaders program running with just a little configuration on Eclipse:
adding User Library JavaFX to the Modulepath for the project, under “Configure Build Path…”,
and adding the VM argument, in the Run configuration: --module-path “c:\Program Files\Java\javafx-jmods-11.0.1” --add-modules=javafx.controls

Of course, the exact details will vary depending on where you put JavaFX.

As far as trying to come up with some sort of standardization for making a jar and including a .bat to run it, I’m thinking the best we can do is maybe use some obvious Environment variable names. Since I installed from openjfx.io (using their tutorial), I am using their suggested names:

PATH_TO_FX
PATH_TO_FX_MODS

Another idea is to set the compilation level to Java 8 in the IDE. In that case, would a non-modular jar work?

I’ve ditched Java 8 and given up on jars for now. If I want to make something “easy” for others to run, I plan to use a jlink file, zipped.


Neat to see a great start to Space Invaders! That used to be my go-to arcade game back in the day. Could go many rounds on just a quarter and attract an audience in the process.


@hekras:
You can set your profile icon at PROFILE / Modify Profile / Look and Layout Preferences

To post a picture, you have to provide a URL, which means the picture has to be online somewhere. That somewhere can be in file space at JGO that comes with your account. To upload there, go to “MY FILES”. The file will need to be a .jpg.

@philfrei
Thanks, now its more like I wanted it to be.

Should I delete the particledemo-source in my first post (calling for a vote here)?