Files
SteamWar/LobbySystem/src/de/steamwar/lobby/particle/DrawnWings.java
T
YoyoNow 4e5945d2bc Lower drawn wings to the shoulder blades
The drawn shapes had their shoulder joint at head height. They now sit
about 1.3 blocks above the feet at the default size.
2026-10-26 10:50:57 +01:00

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;
}
}