diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6ee72b1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2024 ccetl +Copyright (c) 2016-present Barrior + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..af61eb6 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# JParticles for Java + +JParticles for Java is a Java port of the JavaScript library [JParticles](https://github.com/Barrior/JParticles) with +a few enhancements. +It does not include direct rendering code, making it suitable for use in various environments. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..71a29f5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,6 @@ +plugins { + id 'java' +} + +group = 'ccetl' +version = '1.0' \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3e2b5d0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 21 17:37:25 CET 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..53245c5 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'JParticles' + diff --git a/src/main/java/de/ccetl/jparticles/core/Base.java b/src/main/java/de/ccetl/jparticles/core/Base.java new file mode 100644 index 0000000..6612e15 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/core/Base.java @@ -0,0 +1,74 @@ +package de.ccetl.jparticles.core; + +import de.ccetl.jparticles.event.ResizeEvent; +import de.ccetl.jparticles.types.CommonOptions; + +public abstract class Base { + protected final T options; + + protected int width; + protected int height; + + protected boolean paused = true; + + protected long lastUpdate; + + protected Base(T config, int width, int height) { + // Cache the color acquisition function to improve performance + this.options = config; + this.width = width; + this.height = height; + } + + /** + * Bootstrapping + */ + public void bootstrap() { + init(); + } + + /** + * Initializes data or method calls + */ + public abstract void init(); + + /** + * Drawing entry + */ + public abstract void draw(double mouseX, double mouseY); + + /** + * Pauses motion + */ + public void pause() { + if (!paused) { + paused = true; + } + } + + /** + * Starts motion + */ + public void start() { + if (paused) { + lastUpdate = System.currentTimeMillis(); + paused = false; + } + } + + protected double getDelta() { + long time = System.currentTimeMillis(); + long timeDelta = time - lastUpdate; + lastUpdate = time; + return timeDelta / 16.67; + } + + public boolean isPaused() { + return paused; + } + + public void onResize(ResizeEvent event) { + this.width = event.newWidth; + this.height = event.newHeight; + } +} diff --git a/src/main/java/de/ccetl/jparticles/core/Element.java b/src/main/java/de/ccetl/jparticles/core/Element.java new file mode 100644 index 0000000..35d2e3c --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/core/Element.java @@ -0,0 +1,74 @@ +package de.ccetl.jparticles.core; + +import de.ccetl.jparticles.core.shape.Shape; +import de.ccetl.jparticles.event.ResizeEvent; +import de.ccetl.jparticles.util.Vec2d; + +/** + * A particle base for systems. + */ +public abstract class Element extends Vec2d { + /** + * The radius of the element. + */ + private double radius; + /** + * Represents the velocity. + */ + private Vec2d v; + /** + * The color of the element. + */ + private int color; + /** + * The shape. + */ + private Shape shape; + + public Element(double x, double y, double radius, double vx, double vy, int color, Shape shape) { + super(x, y); + this.radius = radius; + this.v = new Vec2d(vx, vy); + this.color = color; + this.shape = shape; + } + + public void onResize(ResizeEvent event, int oldWidth, int oldHeight) { + double ax = getX() / oldWidth; + double ay = getY() / oldHeight; + setX(ax * event.newWidth); + setY(ay * event.newHeight); + } + + public double getRadius() { + return radius; + } + + public double getVx() { + return v.getX(); + } + + public void setVx(double vx) { + this.v.setX(vx); + } + + public double getVy() { + return v.getY(); + } + + public void setVy(double vy) { + this.v.setY(vy); + } + + public Vec2d getV() { + return v; + } + + public int getColor() { + return color; + } + + public Shape getShape() { + return shape; + } +} diff --git a/src/main/java/de/ccetl/jparticles/core/ParticleBase.java b/src/main/java/de/ccetl/jparticles/core/ParticleBase.java new file mode 100644 index 0000000..0e86c5d --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/core/ParticleBase.java @@ -0,0 +1,23 @@ +package de.ccetl.jparticles.core; + +import de.ccetl.jparticles.event.ResizeEvent; +import de.ccetl.jparticles.types.CommonOptions; + +import java.util.LinkedList; +import java.util.List; + +public abstract class ParticleBase extends Base { + protected final List elements = new LinkedList<>(); + + protected ParticleBase(T defaultConfig, int width, int height) { + super(defaultConfig, width, height); + } + + @Override + public void onResize(ResizeEvent event) { + for (E element : elements) { + element.onResize(event, width, height); + } + super.onResize(event); + } +} diff --git a/src/main/java/de/ccetl/jparticles/core/Renderer.java b/src/main/java/de/ccetl/jparticles/core/Renderer.java new file mode 100644 index 0000000..1f1e253 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/core/Renderer.java @@ -0,0 +1,10 @@ +package de.ccetl.jparticles.core; + +public interface Renderer { + void drawLine(double x, double y, double x1, double y1, double width, int color); + void drawLineRotated(double x, double y, double x1, double y1, double translationX, double translationY, double width, double radians, int color); + void drawCircle(double x, double y, double radius, int color); + void drawTriangle(double x, double y, double radius, int color); + void drawStar(double x, double y, double radius, int sides, double dent, int color); + void drawImage(double x, double y, double radius, int id); +} diff --git a/src/main/java/de/ccetl/jparticles/core/shape/Shape.java b/src/main/java/de/ccetl/jparticles/core/shape/Shape.java new file mode 100644 index 0000000..c6cd965 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/core/shape/Shape.java @@ -0,0 +1,53 @@ +package de.ccetl.jparticles.core.shape; + +import de.ccetl.jparticles.core.Renderer; +import de.ccetl.jparticles.util.Vec2d; + +public class Shape { + private final ShapeType type; + /** + * The image id. + */ + private int id; + private int sides; + private double dent; + + public Shape(ShapeType type) { + this.type = type; + } + + public void render(Renderer renderer, Vec2d vec2d, double radius, int color) { + switch (type) { + case CIRCLE: + renderer.drawCircle(vec2d.getX(), vec2d.getY(), radius, color); + break; + case STAR: + renderer.drawStar(vec2d.getX(), vec2d.getY(), radius, sides, dent, color); + break; + case TRIANGLE: + renderer.drawTriangle(vec2d.getX(), vec2d.getY(), radius, color); + break; + case IMAGE: + renderer.drawImage(vec2d.getX(), vec2d.getY(), radius, id); + break; + default: + throw new IllegalArgumentException(); + } + } + + public ShapeType getType() { + return type; + } + + public void setId(int id) { + this.id = id; + } + + public void setSides(int sides) { + this.sides = sides; + } + + public void setDent(double dent) { + this.dent = dent; + } +} diff --git a/src/main/java/de/ccetl/jparticles/core/shape/ShapeType.java b/src/main/java/de/ccetl/jparticles/core/shape/ShapeType.java new file mode 100644 index 0000000..60332cc --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/core/shape/ShapeType.java @@ -0,0 +1,8 @@ +package de.ccetl.jparticles.core.shape; + +public enum ShapeType { + CIRCLE, + TRIANGLE, + STAR, + IMAGE +} diff --git a/src/main/java/de/ccetl/jparticles/event/MouseEvent.java b/src/main/java/de/ccetl/jparticles/event/MouseEvent.java new file mode 100644 index 0000000..ad69014 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/event/MouseEvent.java @@ -0,0 +1,9 @@ +package de.ccetl.jparticles.event; + +public class MouseEvent { + public final double x; + + public MouseEvent(double x) { + this.x = x; + } +} diff --git a/src/main/java/de/ccetl/jparticles/event/ResizeEvent.java b/src/main/java/de/ccetl/jparticles/event/ResizeEvent.java new file mode 100644 index 0000000..96d66dd --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/event/ResizeEvent.java @@ -0,0 +1,11 @@ +package de.ccetl.jparticles.event; + +public class ResizeEvent { + public final int newWidth; + public final int newHeight; + + public ResizeEvent(int newWidth, int newHeight) { + this.newWidth = newWidth; + this.newHeight = newHeight; + } +} diff --git a/src/main/java/de/ccetl/jparticles/systems/LineSystem.java b/src/main/java/de/ccetl/jparticles/systems/LineSystem.java new file mode 100644 index 0000000..8ab871c --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/systems/LineSystem.java @@ -0,0 +1,233 @@ +package de.ccetl.jparticles.systems; + +import de.ccetl.jparticles.core.Base; +import de.ccetl.jparticles.event.MouseEvent; +import de.ccetl.jparticles.types.line.LineElement; +import de.ccetl.jparticles.types.line.LineOptions; +import de.ccetl.jparticles.util.Utils; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Supplier; + +public class LineSystem extends Base { + private final List elements = new LinkedList<>(); + private final int[] specificAngles = {-180, -90, 0, 90, 180}; + + public LineSystem(LineOptions config, int width, int height) { + super(config, width, height); + bootstrap(); + } + + @Override + public void init() { + elements.clear(); + createLines(options.getNumber(), null); + } + + private void createLines(int number, Integer positionX) { + double minWidth = options.getMinWidth(); + double maxWidth = options.getMaxWidth(); + double minSpeed = options.getMinSpeed(); + double maxSpeed = options.getMaxSpeed(); + double minDegree = options.getMinDegree(); + double maxDegree = options.getMaxDegree(); + + while (number-- > 0) { + double width = Utils.getRandomInRange(minWidth, maxWidth); + double speed = Utils.randomSpeed(minSpeed, maxSpeed); + int degree = (int) (Utils.getRandomInRange(minDegree, maxDegree) % 180); + double x = positionX == null ? Utils.getRandomInRange(0, this.width) : positionX; + + elements.add(new LineElement(x, width, options.getColorSupplier().get(), speed, degree)); + } + } + + public void onMouseCLick(MouseEvent event) { + if (!options.isCreateOnClick() || paused) { + return; + } + + createLines(options.getNumberOfCreations(), (int) event.x); + } + + @Override + public void draw(double mouseX, double mouseY) { + double hypotenuse = Math.hypot(width, height); + double lineLength = hypotenuse * 10; + double delta = getDelta(); + + double OC = Math.max(0, options.getOverflowCompensation()); + + List toRemove = new LinkedList<>(); + + for (LineElement line : elements) { + double radian = Math.toRadians(-line.getDegree()); + + double adjacentSide = 0; + if (Arrays.stream(specificAngles).noneMatch(angle -> angle == line.getDegree())) { + adjacentSide = Math.abs(height * 0.5 / Math.tan(radian)); + } + + options.getRenderer().drawLineRotated(-lineLength, 0, lineLength, 0, line.getX(), height * 0.5, line.getWidth(), radian, line.getColor()); + + if (!paused) { + line.setX(line.getX() + line.getSpeed() * delta); + } + + boolean isOverflow = false; + boolean isOverflowOnLeft = false; + + if (line.getX() + adjacentSide + line.getWidth() + OC < 0) { + isOverflow = true; + isOverflowOnLeft = true; + } else if (line.getX() > width + adjacentSide + line.getWidth() + OC) { + isOverflow = true; + } + + if (isOverflow) { + if (options.isRemoveOnOverflow() && elements.size() > options.getReservedLines()) { + toRemove.add(line); + } else { + line.setSpeed(Math.abs(line.getSpeed()) * (isOverflowOnLeft ? 1 : -1)); + } + } + } + + elements.removeAll(toRemove); + } + + public static abstract class DefaultConfig implements LineOptions { + private Supplier colorSupplier = () -> -65536; + private int number = 6; + private double maxWidth = 2; + private double minWidth = 1; + private double maxSpeed = 3; + private double minSpeed = 1; + private double maxDegree = 90; + private double minDegree = 80; + private boolean createOnClick = true; + private int numberOfCreations = 3; + private boolean removeOnOverflow = true; + private double overflowCompensation = 20; + private int reservedLines = 6; + + @Override + public Supplier getColorSupplier() { + return colorSupplier; + } + + public void setColorSupplier(Supplier colorSupplier) { + this.colorSupplier = colorSupplier; + } + + @Override + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + @Override + public double getMaxWidth() { + return maxWidth; + } + + public void setMaxWidth(double maxWidth) { + this.maxWidth = maxWidth; + } + + @Override + public double getMinWidth() { + return minWidth; + } + + public void setMinWidth(double minWidth) { + this.minWidth = minWidth; + } + + @Override + public double getMaxSpeed() { + return maxSpeed; + } + + public void setMaxSpeed(double maxSpeed) { + this.maxSpeed = maxSpeed; + } + + @Override + public double getMinSpeed() { + return minSpeed; + } + + public void setMinSpeed(double minSpeed) { + this.minSpeed = minSpeed; + } + + @Override + public double getMaxDegree() { + return maxDegree; + } + + public void setMaxDegree(double maxDegree) { + this.maxDegree = maxDegree; + } + + @Override + public double getMinDegree() { + return minDegree; + } + + public void setMinDegree(double minDegree) { + this.minDegree = minDegree; + } + + @Override + public boolean isCreateOnClick() { + return createOnClick; + } + + public void setCreateOnClick(boolean createOnClick) { + this.createOnClick = createOnClick; + } + + @Override + public int getNumberOfCreations() { + return numberOfCreations; + } + + public void setNumberOfCreations(int numberOfCreations) { + this.numberOfCreations = numberOfCreations; + } + + @Override + public boolean isRemoveOnOverflow() { + return removeOnOverflow; + } + + public void setRemoveOnOverflow(boolean removeOnOverflow) { + this.removeOnOverflow = removeOnOverflow; + } + + @Override + public double getOverflowCompensation() { + return overflowCompensation; + } + + public void setOverflowCompensation(double overflowCompensation) { + this.overflowCompensation = overflowCompensation; + } + + @Override + public int getReservedLines() { + return reservedLines; + } + + public void setReservedLines(int reservedLines) { + this.reservedLines = reservedLines; + } + } +} diff --git a/src/main/java/de/ccetl/jparticles/systems/ParticleSystem.java b/src/main/java/de/ccetl/jparticles/systems/ParticleSystem.java new file mode 100644 index 0000000..dd72839 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/systems/ParticleSystem.java @@ -0,0 +1,496 @@ +package de.ccetl.jparticles.systems; + +import de.ccetl.jparticles.core.ParticleBase; +import de.ccetl.jparticles.core.shape.Shape; +import de.ccetl.jparticles.core.shape.ShapeType; +import de.ccetl.jparticles.types.particle.*; +import de.ccetl.jparticles.util.Utils; +import de.ccetl.jparticles.util.Vec2d; + +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.function.Supplier; + +public class ParticleSystem extends ParticleBase { + private double positionX; + private double positionY; + private LineShapeMaker lineShapeMaker; + + public ParticleSystem(ParticleOptions defaultConfig, int width, int height) { + super(defaultConfig, width, height); + bootstrap(); + } + + @Override + public void init() { + if (this.options.getRange() > 0) { + this.positionX = Math.random() * width; + this.positionY = Math.random() * height; + this.defineLineShape(); + } + + elements.clear(); + createDots(); + } + + private void defineLineShape() { + double proximity = this.options.getProximity(); + double range = this.options.getRange(); + LineShape lineShape = this.options.getLineShape(); + + if (lineShape == LineShape.CUBE) { + this.lineShapeMaker = (x, y, sx, sy, cb) -> { + double positionX = this.positionX; + double positionY = this.positionY; + if (Math.abs(x - sx) <= proximity && Math.abs(y - sy) <= proximity && Math.abs(x - positionX) <= range && Math.abs(y - positionY) <= range && Math.abs(sx - positionX) <= range && Math.abs(sy - positionY) <= range) { + cb.run(); + } + }; + } else { + this.lineShapeMaker = (x, y, sx, sy, cb) -> { + double positionX = this.positionX; + double positionY = this.positionY; + if (Math.abs(x - sx) <= proximity && Math.abs(y - sy) <= proximity && ((Math.abs(x - positionX) <= range && Math.abs(y - positionY) <= range) || (Math.abs(sx - positionX) <= range && Math.abs(sy - positionY) <= range))) { + cb.run(); + } + }; + } + } + + private void createDots() { + for (int i = elements.size(); i < options.getNumber(); i++) { + createDot(); + } + } + + private void createDot() { + double r = Utils.getRandomInRange(options.getMinRadius(), options.getMaxRadius()); + double minX = r; + double maxX = width - r; + double minY = r; + double maxY = height - r; + switch (options.getSpawnRegion()) { + case ABOVE_RIGHT: + case BELLOW_RIGHT: + case RIGHT: + minX += width; + maxX += width; + break; + case ABOVE_LEFT: + case BELLOW_LEFT: + case LEFT: + minX -= width; + maxX -= width; + break; + } + switch (options.getSpawnRegion()) { + case ABOVE_LEFT: + case ABOVE_RIGHT: + case ABOVE: + minY -= height; + maxY -= height; + break; + case BELLOW: + case BELLOW_LEFT: + case BELLOW_RIGHT: + minY += height; + maxY += height; + break; + } + Vec2d speed = Utils.getSpeed(options.getDirection(), options.getMinSpeed(), options.getMaxSpeed()); + ParticleElement dot = new ParticleElement(r, Utils.getRandomInRange(minX, maxX), Utils.getRandomInRange(minY, maxY), speed.getX(), speed.getY(), options.getColorSupplier().get(), options.getShapeSupplier().get(), options.getParallaxLayer()[new Random().nextInt(options.getParallaxLayer().length)], 0, 0); + + if (options.getCollisionIntern() != Obstacle.IGNORE && elements.stream().anyMatch(dot1 -> Utils.intersect(dot, dot1))) { + try { + createDot(); + } catch (StackOverflowError e) { + e.printStackTrace(); + } + + } + + this.elements.add(dot); + } + + @Override + public void draw(double mouseX, double mouseY) { + positionX = mouseX; + positionY = mouseY; + updateXY(mouseX, mouseY); + + for (ParticleElement dot : this.elements) { + double x = dot.getX() + dot.getParallaxOffsetX(); + double y = dot.getY() + dot.getParallaxOffsetY(); + dot.getShape().render(options.getRenderer(), new Vec2d(x, y), dot.getRadius(), dot.getColor()); + } + + connectDots(); + createDots(); + } + + private void connectDots() { + if (this.options.getRange() <= 0) { + return; + } + + for (ParticleElement dot : elements) { + Vec2d vec = new Vec2d(Utils.getX(dot), Utils.getY(dot)); + + for (ParticleElement dot1 : elements) { + if (dot == dot1) { + continue; + } + + Vec2d vec1 = new Vec2d(Utils.getX(dot1), Utils.getY(dot1)); + + lineShapeMaker.apply(vec.getX(), vec.getY(), vec1.getX(), vec1.getY(), () -> { + double x; + double y; + double x1; + double y1; + + if (options.isCenterLines()) { + x = vec.getX(); + y = vec.getY(); + x1 = vec1.getX(); + y1 = vec1.getY(); + } else { + Vec2d vec2 = vec.copy().set(vec); + + Vec2d deltaVec = vec1.copy().subtract(vec2); + double rotation = -Math.atan2(deltaVec.getX(), deltaVec.getY()); + rotation = Math.toRadians(Math.toDegrees(rotation) + 180); + double correctedRotation = rotation - 0.5 * Math.PI; + double correctedRotation1 = rotation + 0.5 * Math.PI; + + x = Math.round(vec.getX() + Math.cos(correctedRotation) * dot.getRadius()); + y = Math.round(vec.getY() + Math.sin(correctedRotation) * dot.getRadius()); + x1 = Math.round(vec1.getX() + Math.cos(correctedRotation1) * dot1.getRadius()); + y1 = Math.round(vec1.getY() + Math.sin(correctedRotation1) * dot1.getRadius()); + } + + options.getRenderer().drawLine(x, y, x1, y1, options.getLineWidth(), dot.getColor()); + }); + } + } + } + + private void updateXY(double mouseX, double mouseY) { + if (paused) { + return; + } + + boolean parallax = this.options.isParallax(); + double parallaxStrength = this.options.getParallaxStrength(); + double delta = getDelta(); + + List toRemove = new LinkedList<>(); + + for (ParticleElement dot : elements) { + dot.setX(dot.getX() + dot.getVx() * delta); + dot.setY(dot.getY() + dot.getVy() * delta); + + if (parallax) { + double divisor = parallaxStrength * dot.getParallaxLayer(); + double parallaxOffsetX = (mouseX / divisor - dot.getParallaxOffsetX()) / 10; + double parallaxOffsetY = (mouseY / divisor - dot.getParallaxOffsetY()) / 10; + double newX = dot.getX() + parallaxOffsetX; + double newY = dot.getY() + parallaxOffsetY; + + if (options.getCollisionEdge() == Obstacle.IGNORE || newX - dot.getRadius() >= 0 && newX + dot.getRadius() <= width) { + dot.setParallaxOffsetX(parallaxOffsetX); + } else { + double minX = -dot.getX() + dot.getRadius(); + double maxX = width - dot.getX() - dot.getRadius(); + dot.setParallaxOffsetX(Utils.clamp(parallaxOffsetX, minX, maxX)); + } + + if (options.getCollisionEdge() == Obstacle.IGNORE || newY - dot.getRadius() >= 0 && newY + dot.getRadius() <= height) { + dot.setParallaxOffsetY(parallaxOffsetY); + } else { + double minY = -dot.getY() + dot.getRadius(); + double maxY = height - dot.getY() - dot.getRadius(); + dot.setParallaxOffsetY(Utils.clamp(parallaxOffsetY, minY, maxY)); + } + } + + if (options.isHoverRepulse()) { + Vec2d deltaVec = new Vec2d(Utils.getX(dot), Utils.getY(dot)).subtract(new Vec2d(mouseX, mouseY)); + double distance = deltaVec.getLength(); + + if (distance < options.getRepulseRadius()) { + dot.set(deltaVec.add(dot)); + } + } + + double r = dot.getRadius(); + double x = dot.getX(); + double y = dot.getY(); + x += dot.getParallaxOffsetX(); + y += dot.getParallaxOffsetY(); + + boolean movingOutHorizontal = (dot.getVx() > 0 && (x + r >= width)) || (dot.getVx() < 0 && (x - r <= 0)); + boolean movingOutVertical = (dot.getVy() > 0 && (y + r >= height)) || (dot.getVy() < 0 && (y - r <= 0)); + + if (movingOutHorizontal && options.getCollisionEdge() == Obstacle.BOUNCE) { + dot.setVx(-dot.getVx()); + dot.setVelocityChanged(true); + } else if (movingOutHorizontal && (x + r < 0 || x - r > width) && options.getCollisionEdge() == Obstacle.IGNORE) { + toRemove.add(dot); + } + + if (movingOutVertical && options.getCollisionEdge() == Obstacle.BOUNCE) { + dot.setVy(-dot.getVy()); + dot.setVelocityChanged(true); + } else if (movingOutVertical && (y + r < 0 || y - r > height) && options.getCollisionEdge() == Obstacle.IGNORE) { + toRemove.add(dot); + } + } + + elements.removeAll(toRemove); + + if (options.getCollisionIntern() == Obstacle.BOUNCE) { + handleInternCollision(); + } + + if (options.getCollisionEdge() != Obstacle.IGNORE || options.getCollisionIntern() != Obstacle.IGNORE) { + for (ParticleElement dot : elements) { + dot.setVelocityChanged(false); + } + } + } + + private void handleInternCollision() { + for (ParticleElement dot : elements) { + for (ParticleElement dot1 : elements) { + if (dot1 == dot || !Utils.intersect(dot, dot1) || dot1.isVelocityChanged() && dot.isVelocityChanged()) { + continue; + } + + dot.handleCollision(dot1); + } + } + } + +// public void onMouseCLick(MouseEvent event) { +// +// } + + private interface LineShapeMaker { + void apply(double x, double y, double sx, double sy, Runnable cb); + } + + public static abstract class DefaultConfig implements ParticleOptions { + private Supplier shapeSupplier = () -> new Shape(ShapeType.CIRCLE); + private Supplier colorSupplier = () -> -65536; + private Obstacle collisionIntern = Obstacle.IGNORE; + private Obstacle collisionEdge = Obstacle.BOUNCE; + private Direction direction = Direction.RANDOM; + private SpawnRegion spawnRegion = SpawnRegion.INSIDE; + private int number = 20; + private double maxRadius = 2.4; + private double minRadius = 0.6; + private double minSpeed = 0.1; + private double maxSpeed = 1; + private double proximity = 0.2; + private double range = 0.2; + private double lineWidth = 0.2; + private LineShape lineShape = LineShape.SPIDER; + private boolean centerLines = false; + private boolean parallax = false; + private int[] parallaxLayer = {1, 2, 3}; + private double parallaxStrength = 3; + private boolean hoverRepulse = false; + private double repulseRadius = 100; + + @Override + public Supplier getShapeSupplier() { + return shapeSupplier; + } + + public void setShapeSupplier(Supplier shapeSupplier) { + this.shapeSupplier = shapeSupplier; + } + + @Override + public Supplier getColorSupplier() { + return colorSupplier; + } + + public void setColorSupplier(Supplier colorSupplier) { + this.colorSupplier = colorSupplier; + } + + @Override + public Obstacle getCollisionIntern() { + return collisionIntern; + } + + public void setCollisionIntern(Obstacle collisionIntern) { + this.collisionIntern = collisionIntern; + } + + @Override + public Obstacle getCollisionEdge() { + return collisionEdge; + } + + @Override + public Direction getDirection() { + return direction; + } + + public void setDirection(Direction direction) { + this.direction = direction; + } + + public void setCollisionEdge(Obstacle collisionEdge) { + this.collisionEdge = collisionEdge; + } + + @Override + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + @Override + public double getMaxRadius() { + return maxRadius; + } + + public void setMaxRadius(double maxRadius) { + this.maxRadius = maxRadius; + } + + @Override + public double getMinRadius() { + return minRadius; + } + + public void setMinRadius(double minRadius) { + this.minRadius = minRadius; + } + + @Override + public double getMinSpeed() { + return minSpeed; + } + + public void setMinSpeed(double minSpeed) { + this.minSpeed = minSpeed; + } + + @Override + public double getMaxSpeed() { + return maxSpeed; + } + + public void setMaxSpeed(double maxSpeed) { + this.maxSpeed = maxSpeed; + } + + @Override + public double getProximity() { + return proximity; + } + + public void setProximity(double proximity) { + this.proximity = proximity; + } + + @Override + public double getRange() { + return range; + } + + public void setRange(double range) { + this.range = range; + } + + @Override + public double getLineWidth() { + return lineWidth; + } + + public void setLineWidth(double lineWidth) { + this.lineWidth = lineWidth; + } + + @Override + public LineShape getLineShape() { + return lineShape; + } + + public void setLineShape(LineShape lineShape) { + this.lineShape = lineShape; + } + + @Override + public boolean isParallax() { + return parallax; + } + + public void setParallax(boolean parallax) { + this.parallax = parallax; + } + + @Override + public int[] getParallaxLayer() { + return parallaxLayer; + } + + public void setParallaxLayer(int[] parallaxLayer) { + this.parallaxLayer = parallaxLayer; + } + + @Override + public double getParallaxStrength() { + return parallaxStrength; + } + + public void setParallaxStrength(double parallaxStrength) { + this.parallaxStrength = parallaxStrength; + } + + @Override + public boolean isCenterLines() { + return centerLines; + } + + public void setCenterLines(boolean centerLines) { + this.centerLines = centerLines; + } + + @Override + public SpawnRegion getSpawnRegion() { + return spawnRegion; + } + + public void setSpawnRegion(SpawnRegion spawnRegion) { + this.spawnRegion = spawnRegion; + } + + @Override + public boolean isHoverRepulse() { + return hoverRepulse; + } + + public void setHoverRepulse(boolean hoverRepulse) { + this.hoverRepulse = hoverRepulse; + } + + @Override + public double getRepulseRadius() { + return repulseRadius; + } + + public void setRepulseRadius(double repulseRadius) { + this.repulseRadius = repulseRadius; + } + } +} diff --git a/src/main/java/de/ccetl/jparticles/systems/SnowSystem.java b/src/main/java/de/ccetl/jparticles/systems/SnowSystem.java new file mode 100644 index 0000000..d6f6075 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/systems/SnowSystem.java @@ -0,0 +1,273 @@ +package de.ccetl.jparticles.systems; + +import de.ccetl.jparticles.core.ParticleBase; +import de.ccetl.jparticles.core.shape.Shape; +import de.ccetl.jparticles.core.shape.ShapeType; +import de.ccetl.jparticles.types.snow.SnowElement; +import de.ccetl.jparticles.types.snow.SnowOptions; +import de.ccetl.jparticles.util.Utils; +import de.ccetl.jparticles.util.Vec2d; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Supplier; + +public class SnowSystem extends ParticleBase { + private long startTime = System.currentTimeMillis(); + private boolean isFinished = false; + + public SnowSystem(SnowOptions options, int width, int height) { + super(options, width, height); + bootstrap(); + } + + @Override + public void init() { + elements.clear(); + createSnowflakes(options.isFirstRandom()); + } + + @Override + public void draw(double mouseX, double mouseY) { + double maxR = options.getMaxRadius(); + boolean swing = options.isSwing(); + int swingInterval = options.getSwingInterval(); + double swingProbability = options.getSwingProbability(); + double duration = options.getDuration(); + double delta = getDelta(); + + List toRemove = new LinkedList<>(); + List toAdd = new LinkedList<>(); + + for (SnowElement snowflake : elements) { + double x = snowflake.getX(); + double y = snowflake.getY(); + double r = snowflake.getRadius(); + + snowflake.getShape().render(options.getRenderer(), new Vec2d(x, y), r, snowflake.getColor()); + + if (paused) { + return; + } + + snowflake.setX(x + snowflake.getVx() * delta); + snowflake.setY(y + snowflake.getVy() * delta); + + if (swing && System.currentTimeMillis() - snowflake.getSwingAt() > swingInterval && Math.random() < (r / maxR) * swingProbability) { + snowflake.setSwingAt(System.currentTimeMillis()); + snowflake.setVx(-snowflake.getVx()); + } + + if (x + r < 0 || x - r > width) { + if (duration > 0) { + toRemove.add(snowflake); + } else { + toAdd.add(createSnowflake(false)); + } + } else if (y - r > height) { + toRemove.add(snowflake); + } + } + + elements.removeAll(toRemove); + for (SnowElement snowElement : toAdd) { + if (elements.size() < options.getNumber() || !options.isStrict()) { + elements.add(snowElement); + } + } + + if (paused) { + return; + } + + boolean timeEnd = duration > 0 && System.currentTimeMillis() - startTime > duration; + + if (!timeEnd && Math.random() > 0.9) { + createSnowflakes(false); + } + + if (elements.isEmpty()) { + isFinished = true; + onFinish(); + } + } + + private SnowElement createSnowflake(boolean random) { + double maxR = options.getMaxRadius(); + double minR = options.getMinRadius(); + double maxSpeed = options.getMaxSpeed(); + double minSpeed = options.getMinSpeed(); + + double r = Utils.getRandomInRange(minR, maxR); + + return new SnowElement( + r, + Utils.getRandomInRange(0, width), + random ? Utils.getRandomInRange(0, height) : -r, + Utils.randomSpeed(minSpeed, maxSpeed), + Math.abs(r * Utils.randomSpeed(minSpeed, maxSpeed)), + options.getColorSupplier().get(), + System.currentTimeMillis(), + options.getShapeSupplier().get() + ); + } + + private void createSnowflakes(boolean random) { + if (random) { + for (int i = 0; i < options.getNumber(); i++) { + elements.add(createSnowflake(true)); + } + return; + } + + int count = Math.max(0, (int) Math.ceil(Math.random() * options.getNumber())); + while (count-- > 0 && (elements.size() < options.getNumber() || !options.isStrict())) { + elements.add(createSnowflake(false)); + } + } + + public void fallAgain() { + if (!paused && isFinished) { + isFinished = false; + startTime = System.currentTimeMillis(); + createSnowflakes(false); + } + } + + public void onFinish() { + + } + + public static abstract class DefaultConfig implements SnowOptions { + private Supplier colorSupplier = () -> -1; + private Supplier shapeSupplier = () -> new Shape(ShapeType.CIRCLE); + private boolean firstRandom = false; + private boolean strict = false; + private int number = 6; + private double maxRadius = 6.5; + private double minRadius = 0.5; + private double maxSpeed = 0.6; + private double minSpeed = 0.1; + private double duration = 0; + private boolean swing = true; + private int swingInterval = 2000; + private double swingProbability = 0.06; + + @Override + public boolean isFirstRandom() { + return firstRandom; + } + + public void setFirstRandom(boolean firstRandom) { + this.firstRandom = firstRandom; + } + + @Override + public boolean isStrict() { + return strict; + } + + public void setStrict(boolean strict) { + this.strict = strict; + } + + @Override + public Supplier getColorSupplier() { + return colorSupplier; + } + + public void setColorSupplier(Supplier colorSupplier) { + this.colorSupplier = colorSupplier; + } + + @Override + public Supplier getShapeSupplier() { + return shapeSupplier; + } + + public void setShapeSupplier(Supplier shapeSupplier) { + this.shapeSupplier = shapeSupplier; + } + + @Override + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + @Override + public double getMaxRadius() { + return maxRadius; + } + + public void setMaxRadius(double maxRadius) { + this.maxRadius = maxRadius; + } + + @Override + public double getMinRadius() { + return minRadius; + } + + public void setMinRadius(double minRadius) { + this.minRadius = minRadius; + } + + @Override + public double getMaxSpeed() { + return maxSpeed; + } + + public void setMaxSpeed(double maxSpeed) { + this.maxSpeed = maxSpeed; + } + + @Override + public double getMinSpeed() { + return minSpeed; + } + + public void setMinSpeed(double minSpeed) { + this.minSpeed = minSpeed; + } + + @Override + public double getDuration() { + return duration; + } + + public void setDuration(double duration) { + this.duration = duration; + } + + @Override + public boolean isSwing() { + return swing; + } + + public void setSwing(boolean swing) { + this.swing = swing; + } + + @Override + public int getSwingInterval() { + return swingInterval; + } + + public void setSwingInterval(int swingInterval) { + this.swingInterval = swingInterval; + } + + @Override + public double getSwingProbability() { + return swingProbability; + } + + public void setSwingProbability(double swingProbability) { + this.swingProbability = swingProbability; + } + } +} diff --git a/src/main/java/de/ccetl/jparticles/systems/WaveSystem.java b/src/main/java/de/ccetl/jparticles/systems/WaveSystem.java new file mode 100644 index 0000000..11826f7 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/systems/WaveSystem.java @@ -0,0 +1,20 @@ +package de.ccetl.jparticles.systems; + +import de.ccetl.jparticles.core.Base; +import de.ccetl.jparticles.types.wave.WaveOptions; + +public class WaveSystem extends Base { + protected WaveSystem(WaveOptions config, int width, int height) { + super(config, width, height); + } + + @Override + public void init() { + + } + + @Override + public void draw(double mouseX, double mouseY) { + + } +} diff --git a/src/main/java/de/ccetl/jparticles/types/CommonOptions.java b/src/main/java/de/ccetl/jparticles/types/CommonOptions.java new file mode 100644 index 0000000..b599283 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/CommonOptions.java @@ -0,0 +1,11 @@ +package de.ccetl.jparticles.types; + +import de.ccetl.jparticles.core.Renderer; + +import java.util.function.Supplier; + +public interface CommonOptions { + Renderer getRenderer(); + + Supplier getColorSupplier(); +} diff --git a/src/main/java/de/ccetl/jparticles/types/CommonParticleOptions.java b/src/main/java/de/ccetl/jparticles/types/CommonParticleOptions.java new file mode 100644 index 0000000..48a236c --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/CommonParticleOptions.java @@ -0,0 +1,27 @@ +package de.ccetl.jparticles.types; + +import de.ccetl.jparticles.core.shape.Shape; + +import java.util.function.Supplier; + +public interface CommonParticleOptions extends CommonOptions { + int getNumber(); + + // Maximum radius of particles + // (0, +∞) + double getMaxRadius(); + + // Minimum radius of particles + // (0, +∞) + double getMinRadius(); + + // Minimum speed of particles + // (0, +∞) + double getMinSpeed(); + + // Maximum speed of particles + // (0, +∞) + double getMaxSpeed(); + + Supplier getShapeSupplier(); +} diff --git a/src/main/java/de/ccetl/jparticles/types/line/LineElement.java b/src/main/java/de/ccetl/jparticles/types/line/LineElement.java new file mode 100644 index 0000000..5c1898f --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/line/LineElement.java @@ -0,0 +1,49 @@ +package de.ccetl.jparticles.types.line; + +public class LineElement { + private double x; + private double width; + private int color; + private double speed; + private int degree; + + public LineElement(double x, double width, int color, double speed, int degree) { + this.x = x; + this.width = width; + this.color = color; + this.speed = speed; + this.degree = degree; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getWidth() { + return width; + } + + public void setWidth(double width) { + this.width = width; + } + + public int getColor() { + return color; + } + + public double getSpeed() { + return speed; + } + + public void setSpeed(double speed) { + this.speed = speed; + } + + public int getDegree() { + return degree; + } +} diff --git a/src/main/java/de/ccetl/jparticles/types/line/LineOptions.java b/src/main/java/de/ccetl/jparticles/types/line/LineOptions.java new file mode 100644 index 0000000..db9fa67 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/line/LineOptions.java @@ -0,0 +1,41 @@ +package de.ccetl.jparticles.types.line; + +import de.ccetl.jparticles.types.CommonOptions; + +public interface LineOptions extends CommonOptions { + // Number of lines + int getNumber(); + + // Maximum width + double getMaxWidth(); + + // Minimum width + double getMinWidth(); + + // Maximum speed + double getMaxSpeed(); + + // Minimum speed + double getMinSpeed(); + + // Maximum inclination angle of the line [0, 180] + double getMaxDegree(); + + // Minimum inclination angle + double getMinDegree(); + + // Create line on click + boolean isCreateOnClick(); + + // Number of lines to create + int getNumberOfCreations(); + + // Remove lines on overflow + boolean isRemoveOnOverflow(); + + // Overflow compensation, to let the line overflow the container by a certain distance (unit: PX), value range: [0, +∞) + double getOverflowCompensation(); + + // Number of lines to be retained to avoid all being removed, effective when removeOnOverflow is true + int getReservedLines(); +} diff --git a/src/main/java/de/ccetl/jparticles/types/particle/Direction.java b/src/main/java/de/ccetl/jparticles/types/particle/Direction.java new file mode 100644 index 0000000..29bf783 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/particle/Direction.java @@ -0,0 +1,13 @@ +package de.ccetl.jparticles.types.particle; + +public enum Direction { + RANDOM, + UP, + UP_RIGHT, + RIGHT, + DOWN_RIGHT, + DOWN, + DOWN_LEFT, + LEFT, + UP_LEFT +} diff --git a/src/main/java/de/ccetl/jparticles/types/particle/LineShape.java b/src/main/java/de/ccetl/jparticles/types/particle/LineShape.java new file mode 100644 index 0000000..4789169 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/particle/LineShape.java @@ -0,0 +1,6 @@ +package de.ccetl.jparticles.types.particle; + +public enum LineShape { + CUBE, + SPIDER +} diff --git a/src/main/java/de/ccetl/jparticles/types/particle/Obstacle.java b/src/main/java/de/ccetl/jparticles/types/particle/Obstacle.java new file mode 100644 index 0000000..2bc4539 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/particle/Obstacle.java @@ -0,0 +1,6 @@ +package de.ccetl.jparticles.types.particle; + +public enum Obstacle { + BOUNCE, + IGNORE +} diff --git a/src/main/java/de/ccetl/jparticles/types/particle/ParticleElement.java b/src/main/java/de/ccetl/jparticles/types/particle/ParticleElement.java new file mode 100644 index 0000000..9bb702d --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/particle/ParticleElement.java @@ -0,0 +1,97 @@ +package de.ccetl.jparticles.types.particle; + +import de.ccetl.jparticles.core.Element; +import de.ccetl.jparticles.core.shape.Shape; +import de.ccetl.jparticles.util.Utils; +import de.ccetl.jparticles.util.Vec2d; + +public class ParticleElement extends Element { + protected int parallaxLayer; + protected double parallaxOffsetX; + protected double parallaxOffsetY; + protected boolean velocityChanged; + + public ParticleElement(double radius, double x, double y, double vx, double vy, int color, Shape shape, int parallaxLayer, double parallaxOffsetX, double parallaxOffsetY) { + super(x, y, radius, vx, vy, color, shape); + this.parallaxLayer = parallaxLayer; + this.parallaxOffsetX = parallaxOffsetX; + this.parallaxOffsetY = parallaxOffsetY; + } + + public Vec2d subtract(ParticleElement other) { + return new Vec2d(Utils.getX(this) - Utils.getX(other), Utils.getY(this) - Utils.getY(other)); + } + + @Override + public Vec2d normalize() { + double length = getLength(); + if (length != 0) { + return new Vec2d(Utils.getX(this) / length, Utils.getY(this) / length); + } else { + return ORDINAL.copy(); + } + } + + @Override + public double getLength() { + return Math.sqrt(Utils.sq(Utils.getX(this)) + Utils.sq(Utils.getY(this))); + } + + public void handleCollision(ParticleElement other) { + if (this.isVelocityChanged() && other.isVelocityChanged()) { + return; + } + + Vec2d collisionNormal = other.subtract(this).normalize(); + Vec2d relativeVelocity = other.getV().copy().subtract(getV()); + double relativeVelocityDotProduct = collisionNormal.dot(new Vec2d(relativeVelocity.getX(), relativeVelocity.getY())); + if (relativeVelocityDotProduct > 0) { + return; + } + + double impulseScalar = -2 * relativeVelocityDotProduct; + Vec2d impulse = collisionNormal.scale(impulseScalar); + if (!this.isVelocityChanged()) { + double oldSpeed = this.getV().getLength(); + this.getV().subtract(impulse); + double scaleFactor = oldSpeed / this.getV().getLength(); + this.getV().multiply(scaleFactor); + this.setVelocityChanged(true); + } + if (!other.isVelocityChanged()) { + double oldSpeed = other.getV().getLength(); + other.getV().add(impulse); + double scaleFactor = oldSpeed / other.getV().getLength(); + other.getV().multiply(scaleFactor); + other.setVelocityChanged(true); + } + } + + public int getParallaxLayer() { + return parallaxLayer; + } + + public double getParallaxOffsetX() { + return parallaxOffsetX; + } + + public void setParallaxOffsetX(double parallaxOffsetX) { + this.parallaxOffsetX = parallaxOffsetX; + } + + public double getParallaxOffsetY() { + return parallaxOffsetY; + } + + public void setParallaxOffsetY(double parallaxOffsetY) { + this.parallaxOffsetY = parallaxOffsetY; + } + + public boolean isVelocityChanged() { + return velocityChanged; + } + + public void setVelocityChanged(boolean velocityChanged) { + this.velocityChanged = velocityChanged; + } +} diff --git a/src/main/java/de/ccetl/jparticles/types/particle/ParticleOptions.java b/src/main/java/de/ccetl/jparticles/types/particle/ParticleOptions.java new file mode 100644 index 0000000..1b04d98 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/particle/ParticleOptions.java @@ -0,0 +1,53 @@ +package de.ccetl.jparticles.types.particle; + +import de.ccetl.jparticles.types.CommonParticleOptions; + +public interface ParticleOptions extends CommonParticleOptions { + // Maximum value of two-point connection + // If the distance between two points within the range is less than proximity, a connection will be made between the two points + // (0, 1) represents the number of times the container width, 0 & [1, +∞) represent specific numbers + double getProximity(); + + // Range of anchor points, more connections with larger range + // When range is 0, no connections are made, and related values are invalid + double getRange(); + + // Line width + double getLineWidth(); + + // Shape of the line + // spider: scattered spider shape + // cube: closed cube shape + LineShape getLineShape(); + + // center lines are faster + boolean isCenterLines(); + + // Parallax effect + boolean isParallax(); + + // Define the number of layers and the level size of each layer in the parallax layer similar to z-index in CSS. + // Range: [0, +∞), the smaller the value, the stronger the parallax effect, 0 means no movement. + // Example of defining four layers of particles: [1, 3, 5, 10] + int[] getParallaxLayer(); + + // Parallax intensity, the smaller the value, the stronger the parallax effect + double getParallaxStrength(); + + // What happens if two particles bump into each other + Obstacle getCollisionIntern(); + + // Reaction on hitting the window boundaries + Obstacle getCollisionEdge(); + + // The initial direction + Direction getDirection(); + + // Where new particles are created + // Mismatching with the direction, you might never see particles + SpawnRegion getSpawnRegion(); + + boolean isHoverRepulse(); + + double getRepulseRadius(); +} diff --git a/src/main/java/de/ccetl/jparticles/types/particle/SpawnRegion.java b/src/main/java/de/ccetl/jparticles/types/particle/SpawnRegion.java new file mode 100644 index 0000000..2fd4ea5 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/particle/SpawnRegion.java @@ -0,0 +1,13 @@ +package de.ccetl.jparticles.types.particle; + +public enum SpawnRegion { + INSIDE, + ABOVE, + BELLOW, + LEFT, + RIGHT, + ABOVE_LEFT, + ABOVE_RIGHT, + BELLOW_RIGHT, + BELLOW_LEFT +} diff --git a/src/main/java/de/ccetl/jparticles/types/snow/SnowElement.java b/src/main/java/de/ccetl/jparticles/types/snow/SnowElement.java new file mode 100644 index 0000000..844b0da --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/snow/SnowElement.java @@ -0,0 +1,21 @@ +package de.ccetl.jparticles.types.snow; + +import de.ccetl.jparticles.core.Element; +import de.ccetl.jparticles.core.shape.Shape; + +public class SnowElement extends Element { + private long swingAt; + + public SnowElement(double radius, double x, double y, double vx, double vy, int color, long swingAt, Shape shape) { + super(x, y, radius, vx, vy, color, shape); + this.swingAt = swingAt; + } + + public long getSwingAt() { + return swingAt; + } + + public void setSwingAt(long swingAt) { + this.swingAt = swingAt; + } +} diff --git a/src/main/java/de/ccetl/jparticles/types/snow/SnowOptions.java b/src/main/java/de/ccetl/jparticles/types/snow/SnowOptions.java new file mode 100644 index 0000000..b27fc87 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/snow/SnowOptions.java @@ -0,0 +1,23 @@ +package de.ccetl.jparticles.types.snow; + +import de.ccetl.jparticles.types.CommonParticleOptions; + +public interface SnowOptions extends CommonParticleOptions { + // Duration of particle existence + double getDuration(); + + // Spawns all snowflakes random at the start + boolean isFirstRandom(); + + // Limits the snowflakes to the number + boolean isStrict(); + + // Whether to randomly change the direction of falling + boolean isSwing(); + + // Time interval for changing direction, in milliseconds + int getSwingInterval(); + + // Probability of changing direction (after reaching the time interval), range [0, 1] + double getSwingProbability(); +} diff --git a/src/main/java/de/ccetl/jparticles/types/wave/WaveOptions.java b/src/main/java/de/ccetl/jparticles/types/wave/WaveOptions.java new file mode 100644 index 0000000..6d376a2 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/types/wave/WaveOptions.java @@ -0,0 +1,48 @@ +package de.ccetl.jparticles.types.wave; + +import de.ccetl.jparticles.types.CommonOptions; + +import java.util.List; + +public interface WaveOptions extends CommonOptions { + // Number of ripples + int getNumber(); + + // Whether to fill the background color, setting to false makes related values invalid + boolean isFill(); + + // Fill color, effective when fill is set to true + List getFillColor(); + + // Whether to draw the border, setting to false makes related values invalid + boolean isLine(); + + // Border color, effective when line is set to true + List getLineColor(); + + // Border width, an empty array results in random width [.2, 2). + List getLineWidth(); + + // Horizontal offset of the ripple, offset value from the left edge of the Canvas + // (0, 1) represents multiple of container width, 0 & [1, +∞) represents specific values + List getOffsetLeft(); + + // Vertical offset of the ripple, distance from the midpoint of the ripple to the top of the Canvas + // (0, 1) represents multiple of container height, 0 & [1, +∞) represents specific values + List getOffsetTop(); + + // Crest height, (0, 1) represents multiple of container height, 0 & [1, +∞) represents specific values + List getCrestHeight(); + + // Number of crests, i.e., number of sine cycles, default random [1, 0.2 * container width) + List getCrestCount(); + + // Movement speed, default random [.1, .4) + List getSpeed(); + + // Mask: image URL address, Base64 format, canvas image source + String getMask(); + + // Mask mode, default normal + String getMaskMode(); +} diff --git a/src/main/java/de/ccetl/jparticles/util/Utils.java b/src/main/java/de/ccetl/jparticles/util/Utils.java new file mode 100644 index 0000000..dbad0c7 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/util/Utils.java @@ -0,0 +1,133 @@ +package de.ccetl.jparticles.util; + +import de.ccetl.jparticles.core.shape.ShapeType; +import de.ccetl.jparticles.types.particle.Direction; +import de.ccetl.jparticles.types.particle.ParticleElement; + +import java.util.Random; + +public class Utils { + private static final Random random = new Random(); + + public static double sq(double d) { + return d * d; + } + + public static double clamp(double value, double min, double max) { + return Math.max(min, Math.min(max, value)); + } + + public static double getRandomInRange(double min, double max) { + double range = max - min; + double scaled = min + random.nextDouble() * range; + return Math.min(max, scaled); + } + + public static Vec2d getSpeed(Direction direction, double min, double max) { + double x = getRandomInRange(min, max); + double y = getRandomInRange(min, max); + switch (direction) { + case UP: + return new Vec2d(0, -y); + case LEFT: + return new Vec2d(-x, 0); + case RIGHT: + return new Vec2d(x, 0); + case DOWN: + return new Vec2d(0, y); + case UP_LEFT: + return new Vec2d(-x, -y); + case UP_RIGHT: + return new Vec2d(x, -y); + case DOWN_LEFT: + return new Vec2d(-x, y); + case DOWN_RIGHT: + return new Vec2d(x, y); + case RANDOM: + return new Vec2d(randomizeDirection(x), randomizeDirection(y)); + default: + throw new IllegalArgumentException(); + } + } + + public static double randomSpeed(double min, double max) { + return randomizeDirection(getRandomInRange(min, max)); + } + + private static int randomizeDirection(double d) { + return d * Math.random() >= 0.5 ? 1 : -1; + } + + public static boolean intersect(ParticleElement element, ParticleElement other) { + ShapeType a = element.getShape().getType(); + ShapeType b = other.getShape().getType(); + + boolean otherRound = b == ShapeType.CIRCLE || b == ShapeType.STAR; + if (a == ShapeType.CIRCLE || a == ShapeType.STAR) { + if (otherRound) { + return intersectCircleCircle(element, other); + } else { + return intersectCircleRectangle(element, other); + } + } else { + if (otherRound) { + return intersectCircleRectangle(other, element); + } else { + return intersectRectangleRectangle(element, other); + } + } + } + + private static boolean intersectCircleCircle(ParticleElement circle, ParticleElement circle1) { + double dx = sq(getX(circle) - getX(circle1)); + double dy = sq(getY(circle) - getY(circle1)); + double distance = Math.sqrt(dx + dy); + return distance <= circle.getRadius() + circle1.getRadius(); + } + + public static boolean intersectCircleRectangle(ParticleElement circle, ParticleElement rectangle) { + double cx = getX(circle); + double cy = getY(circle); + + double rx = getX(rectangle) - rectangle.getRadius(); + double ry = getY(rectangle) - rectangle.getRadius(); + double rx1 = getX(rectangle) + rectangle.getRadius(); + double ry1 = getY(rectangle) + rectangle.getRadius(); + + if (cx >= rx && cx <= rx1 && cy >= ry && cy <= ry1) { + return true; + } + + double radiusSq = sq(circle.getRadius()); + + return intersectCirclePoint(rx, ry, circle, radiusSq) || intersectCirclePoint(rx, ry1, circle, radiusSq) || intersectCirclePoint(rx1, ry, circle, radiusSq) || intersectCirclePoint(rx1, ry1, circle, radiusSq); + } + + private static boolean intersectCirclePoint(double x, double y, ParticleElement circle, double radiusSq) { + double dx = sq(x - getX(circle)); + double dy = sq(y - getY(circle)); + return dx + dy <= radiusSq; + } + + private static boolean intersectRectangleRectangle(ParticleElement rectangle, ParticleElement rectangle1) { + double aX = getX(rectangle) - rectangle.getRadius(); + double aY = getY(rectangle) - rectangle.getRadius(); + double aX1 = getX(rectangle) + rectangle.getRadius(); + double aY1 = getY(rectangle) + rectangle.getRadius(); + + double bX = getX(rectangle1) - rectangle1.getRadius(); + double bY = getY(rectangle1) - rectangle1.getRadius(); + double bX1 = getX(rectangle1) + rectangle1.getRadius(); + double bY1 = getY(rectangle1) + rectangle1.getRadius(); + + return aX <= bX1 && aX1 >= bX && aY <= bY1 && aY1 >= bY; + } + + public static double getX(ParticleElement element) { + return element.getX() + element.getParallaxOffsetX(); + } + + public static double getY(ParticleElement element) { + return element.getY() + element.getParallaxOffsetY(); + } +} diff --git a/src/main/java/de/ccetl/jparticles/util/Vec2d.java b/src/main/java/de/ccetl/jparticles/util/Vec2d.java new file mode 100644 index 0000000..95ce4d2 --- /dev/null +++ b/src/main/java/de/ccetl/jparticles/util/Vec2d.java @@ -0,0 +1,89 @@ +package de.ccetl.jparticles.util; + +/** + * A point on the screen. + */ +public class Vec2d { + public static final Vec2d ORDINAL = new Vec2d(0, 0); + + private double x; + private double y; + + public Vec2d(double x, double y) { + this.x = x; + this.y = y; + } + + public Vec2d add(Vec2d other) { + this.x += other.x; + this.y += other.y; + return this; + } + + public Vec2d subtract(Vec2d other) { + this.x -= other.x; + this.y -= other.y; + return this; + } + + public Vec2d multiply(double d) { + x *= d; + y *= d; + return this; + } + + public Vec2d divide(double d) { + x /= d; + y /= d; + return this; + } + + public double dot(Vec2d other) { + return this.x * other.x + this.y * other.y; + } + + public Vec2d normalize() { + double length = getLength(); + if (length != 0) { + x /= length; + y /= length; + return this; + } else { + return set(ORDINAL); + } + } + + public double getLength() { + return Math.sqrt(Utils.sq(x) + Utils.sq(y)); + } + + public Vec2d scale(double scalar) { + return new Vec2d(this.x * scalar, this.y * scalar); + } + + public Vec2d set(Vec2d other) { + this.x = other.x; + this.y = other.y; + return this; + } + + public Vec2d copy() { + return new Vec2d(x, y); + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } +}