forked from SteamWar/SteamWar
The drawn shapes had their shoulder joint at head height. They now sit about 1.3 blocks above the feet at the default size.
191 lines
8.4 KiB
Java
191 lines
8.4 KiB
Java
/*
|
|
* This file is a part of the SteamWar software.
|
|
*
|
|
* Copyright (C) 2026 SteamWar.de-Serverteam
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package de.steamwar.lobby.particle;
|
|
|
|
import org.bukkit.util.Vector;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
// Wing shapes drawn from lines and curves instead of images, so their number of points can be chosen.
|
|
// Coordinates use the same units as the wing images: x is sideways from the spine, y upwards, about 11 by 13.
|
|
public enum DrawnWings {
|
|
|
|
// A curved upper edge, a row of covert feathers and long flight feathers ending in a scalloped edge
|
|
ANGEL {
|
|
@Override
|
|
List<List<Vector>> rightWing() {
|
|
List<List<Vector>> lines = new ArrayList<>();
|
|
lines.add(curve(p(0.5, 7), p(3, 10), p(6, 11.8), p(9, 12.4), p(10.8, 11.2), p(11, 8.5)));
|
|
lines.add(curve(p(1, 6.6), p(4, 8.3), p(7, 9.3), p(10, 9.8)));
|
|
lines.add(lineThrough(p(0.5, 5.5), p(2.2, 3.8), p(3.4, 4.6), p(4.2, 2.8), p(5.4, 3.9), p(6.3, 2.6), p(7.5, 4),
|
|
p(8.3, 3.4), p(9.4, 5.2), p(10, 5), p(10.7, 6.6), p(11, 7.5), p(11, 8.5)));
|
|
return lines;
|
|
}
|
|
},
|
|
// An arm bone along the top, fingers spreading from the wrist and skin hanging between them
|
|
BAT {
|
|
@Override
|
|
List<List<Vector>> rightWing() {
|
|
List<List<Vector>> lines = new ArrayList<>();
|
|
lines.add(curve(p(0.5, 7), p(3, 9.5), p(6, 11), p(11, 12.5)));
|
|
lines.add(curve(p(6, 11), p(7, 4)));
|
|
lines.add(curve(p(6, 11), p(9, 6)));
|
|
lines.add(curve(p(6, 11), p(10.8, 9)));
|
|
lines.add(curve(p(0.5, 5), p(3.5, 4.6), p(5.5, 3.4), p(7, 4), p(7.6, 5.4), p(9, 6), p(9.6, 7.6), p(10.8, 9), p(11.2, 10.8), p(11, 12.5)));
|
|
return lines;
|
|
}
|
|
},
|
|
// The butterfly curve r = e^cos(t) - 2 cos(4t), which already holds both wings; its small loops around the body are left out
|
|
BUTTERFLY {
|
|
@Override
|
|
List<List<Vector>> rightWing() {
|
|
List<List<Vector>> lines = new ArrayList<>();
|
|
List<Vector> current = new ArrayList<>();
|
|
for (int i = 0; i <= 400; i++) {
|
|
double t = Math.PI * i / 400;
|
|
double r = Math.exp(Math.cos(t)) - 2 * Math.cos(4 * t);
|
|
if (Math.abs(r) < 0.9) {
|
|
if (current.size() > 1) lines.add(current);
|
|
current = new ArrayList<>();
|
|
continue;
|
|
}
|
|
current.add(p(Math.sin(t) * r * 3, Math.cos(t) * r * 2 + 6));
|
|
}
|
|
if (current.size() > 1) lines.add(current);
|
|
return lines;
|
|
}
|
|
},
|
|
// Two thin rounded pairs, a long one above and a short one below
|
|
FAIRY {
|
|
@Override
|
|
List<List<Vector>> rightWing() {
|
|
return List.of(ellipse(5.2, 9.3, 5, 1.9, 35), ellipse(3.8, 4.6, 3.6, 1.5, -30));
|
|
}
|
|
},
|
|
// An upswept lower edge with flame tongues rising from it, each bending out and curling back at the tip
|
|
PHOENIX {
|
|
@Override
|
|
List<List<Vector>> rightWing() {
|
|
List<List<Vector>> lines = new ArrayList<>();
|
|
lines.add(curve(p(0.5, 5), p(4, 5.2), p(8, 7), p(11, 10), p(12, 13)));
|
|
double[][] tongues = {{1.5, 5.2, 2.5, 10}, {4, 5.4, 5.5, 11.5}, {6.5, 6.2, 8.2, 12.8}, {9, 8, 10.5, 14}};
|
|
for (double[] tongue : tongues) {
|
|
lines.add(curve(p(tongue[0], tongue[1]), p(tongue[0] + 1.2, tongue[1] + 1.8), p(tongue[2] - 0.8, tongue[3] - 1.5), p(tongue[2], tongue[3])));
|
|
}
|
|
return lines;
|
|
}
|
|
},
|
|
;
|
|
|
|
// The right wing as lines; the left wing mirrors it
|
|
abstract List<List<Vector>> rightWing();
|
|
|
|
private static final Map<String, WingDesign> CACHE = new HashMap<>();
|
|
// The shapes are drawn with the shoulder joint at y = 7; moving it to 3.3 puts it at the shoulder blades
|
|
// (about 1.3 blocks above the feet at the default size) instead of at head height
|
|
private static final double SHOULDER_SHIFT = 7 - 3.3;
|
|
|
|
// Both wings with the given number of points per wing, spread evenly along the lines
|
|
public WingDesign withDetail(int pointsPerWing) {
|
|
return CACHE.computeIfAbsent(name() + "@" + pointsPerWing, key -> {
|
|
List<Vector> right = sample(rightWing(), pointsPerWing);
|
|
right.forEach(point -> point.setY(point.getY() - SHOULDER_SHIFT));
|
|
List<Vector> both = new ArrayList<>(right);
|
|
right.forEach(point -> both.add(new Vector(-point.getX(), point.getY(), 0)));
|
|
return new WingDesign.WingDesignImpl(both.toArray(new Vector[0]));
|
|
});
|
|
}
|
|
|
|
private static List<Vector> sample(List<List<Vector>> lines, int count) {
|
|
double total = lines.stream().mapToDouble(DrawnWings::length).sum();
|
|
List<Vector> points = new ArrayList<>();
|
|
for (int i = 0; i < count; i++) {
|
|
double distance = total * (i + 0.5) / count;
|
|
for (List<Vector> line : lines) {
|
|
double length = length(line);
|
|
if (distance <= length) {
|
|
points.add(at(line, distance));
|
|
break;
|
|
}
|
|
distance -= length;
|
|
}
|
|
}
|
|
return points;
|
|
}
|
|
|
|
private static double length(List<Vector> line) {
|
|
double length = 0;
|
|
for (int i = 1; i < line.size(); i++) length += line.get(i).distance(line.get(i - 1));
|
|
return length;
|
|
}
|
|
|
|
private static Vector at(List<Vector> line, double distance) {
|
|
for (int i = 1; i < line.size(); i++) {
|
|
double segment = line.get(i).distance(line.get(i - 1));
|
|
if (distance <= segment) {
|
|
return line.get(i - 1).clone().add(line.get(i).clone().subtract(line.get(i - 1)).multiply(segment == 0 ? 0 : distance / segment));
|
|
}
|
|
distance -= segment;
|
|
}
|
|
return line.get(line.size() - 1).clone();
|
|
}
|
|
|
|
private static Vector p(double x, double y) {
|
|
return new Vector(x, y, 0);
|
|
}
|
|
|
|
// A smooth curve through the given points (Catmull-Rom), as many short segments
|
|
private static List<Vector> curve(Vector... points) {
|
|
List<Vector> result = new ArrayList<>();
|
|
for (int i = 0; i < points.length - 1; i++) {
|
|
Vector p0 = points[Math.max(0, i - 1)], p1 = points[i], p2 = points[i + 1], p3 = points[Math.min(points.length - 1, i + 2)];
|
|
for (int step = 0; step < 16; step++) {
|
|
double t = step / 16.0, t2 = t * t, t3 = t2 * t;
|
|
result.add(p1.clone().multiply(2)
|
|
.add(p2.clone().subtract(p0).multiply(t))
|
|
.add(p0.clone().multiply(2).subtract(p1.clone().multiply(5)).add(p2.clone().multiply(4)).subtract(p3).multiply(t2))
|
|
.add(p1.clone().multiply(3).subtract(p0).subtract(p2.clone().multiply(3)).add(p3).multiply(t3))
|
|
.multiply(0.5));
|
|
}
|
|
}
|
|
result.add(points[points.length - 1].clone());
|
|
return result;
|
|
}
|
|
|
|
// Straight segments between the points, for sharp corners like feather tips
|
|
private static List<Vector> lineThrough(Vector... points) {
|
|
return new ArrayList<>(List.of(points));
|
|
}
|
|
|
|
private static List<Vector> ellipse(double centerX, double centerY, double radiusX, double radiusY, double tiltDegrees) {
|
|
double tilt = Math.toRadians(tiltDegrees);
|
|
List<Vector> result = new ArrayList<>();
|
|
for (int i = 0; i <= 64; i++) {
|
|
double a = 2 * Math.PI * i / 64;
|
|
double x = Math.cos(a) * radiusX, y = Math.sin(a) * radiusY;
|
|
result.add(p(centerX + x * Math.cos(tilt) - y * Math.sin(tilt), centerY + x * Math.sin(tilt) + y * Math.cos(tilt)));
|
|
}
|
|
return result;
|
|
}
|
|
}
|