mirror of
https://github.com/Anuken/Mindustry.git
synced 2026-09-22 13:01:12 +03:00
Support for dynamically-generated rounded/angled corners in UI
This commit is contained in:
190
core/src/mindustry/graphics/CornerGenerator.java
Normal file
190
core/src/mindustry/graphics/CornerGenerator.java
Normal file
@@ -0,0 +1,190 @@
|
||||
package mindustry.graphics;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.graphics.g2d.PixmapPacker.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import mindustry.core.*;
|
||||
|
||||
public class CornerGenerator{
|
||||
/** Sub-samples per axis, per pixel, used to antialias the diagonal edge of the sharp/beveled corners. 8 -> 64 samples/pixel. */
|
||||
private static final int supersample = 8;
|
||||
|
||||
private CornerGenerator(){}
|
||||
|
||||
/**
|
||||
* Generates a NinePatchDrawable with antialiased angled corners.
|
||||
* @param unscaledSize size of the corner in unscaled pixels; this is scaled by Scl.scl.
|
||||
* */
|
||||
public static NinePatchDrawable generateAngled(int unscaledSize){
|
||||
int size = Math.max((int)Scl.scl(unscaledSize), 1);
|
||||
TextureRegion[] tiles = renderCorner("corner-" + size, size, (x, y) -> coverage(x, y, size));
|
||||
return ninePatch(tiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an {@link OutlinedCornerDrawable}: an antialiased, beveled-corner outline in the same visual style as
|
||||
* {@link #generateAngled(int)}, but drawing only the outline (stroke) of the shape rather than filling it.
|
||||
* @param unscaledSize size of the corner in unscaled pixels; this is scaled by Scl.scl, same as {@link #generateAngled(int)}.
|
||||
* @param unscaledStroke width of the outline, perpendicular to the line, in unscaled pixels; this is scaled by Scl.scl.
|
||||
*/
|
||||
public static OutlinedCornerDrawable generateAngledOutline(int unscaledSize, float unscaledStroke){
|
||||
int size = Math.max((int)Scl.scl(unscaledSize), 1);
|
||||
float stroke = Scl.scl(unscaledStroke);
|
||||
String name = "corner-outline-" + size + "-" + Float.floatToIntBits(stroke);
|
||||
TextureRegion[] tiles = renderCorner(name, size, (x, y) -> outlineCoverage(x, y, stroke));
|
||||
return outlinedDrawable(tiles, size, stroke);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a NinePatchDrawable with antialiased rounded corners.
|
||||
* @param unscaledSize size of the corner in unscaled pixels; this is scaled by Scl.scl, same as {@link #generateAngled(int)}.
|
||||
*/
|
||||
public static NinePatchDrawable generateRound(int unscaledSize){
|
||||
int size = Math.max((int)Scl.scl(unscaledSize), 1);
|
||||
String name = "corner-round-" + size;
|
||||
TextureRegion[] tiles = renderCorner(name, size, (x, y) -> roundCoverage(x, y, size));
|
||||
return ninePatch(tiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an outlined drawable with antialiased rounded corners.
|
||||
* <p>
|
||||
* @param unscaledSize size of the corner in unscaled pixels; this is scaled by Scl.scl.
|
||||
* @param unscaledStroke width of the outline, in unscaled pixels; this is scaled by Scl.scl.
|
||||
*/
|
||||
public static OutlinedCornerDrawable generateRoundOutline(int unscaledSize, float unscaledStroke){
|
||||
int size = Math.max((int)Scl.scl(unscaledSize), 1);
|
||||
float stroke = Scl.scl(unscaledStroke);
|
||||
String name = "corner-round-outline-" + size + "-" + Float.floatToIntBits(stroke);
|
||||
TextureRegion[] tiles = renderCorner(name, size, (x, y) -> outlineRoundCoverage(x, y, size, stroke));
|
||||
return outlinedDrawable(tiles, size, stroke);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs a {@code size} x {@code size} tile into the UI packer, filling it pixel-by-pixel via {@code fn}, and
|
||||
* returns the four flipped corner regions derived from it.
|
||||
* @return {topLeft, topRight, bottomLeft, bottomRight}, matching the flips used by both {@link #generateAngled(int)}
|
||||
* and {@link #generateAngledOutline(int, float)}.
|
||||
*/
|
||||
private static TextureRegion[] renderCorner(String name, int size, CoverageFn fn){
|
||||
PixmapPacker packer = UI.packer;
|
||||
Rect rect = packer.pack(name, size, size);
|
||||
Page page = packer.getPage(name);
|
||||
Pixmap pix = page.image;
|
||||
|
||||
int baseX = (int)rect.x, baseY = (int)rect.y;
|
||||
|
||||
//draw the antialiased shape directly into the packer page's pixmap
|
||||
for(int y = 0; y < size; y++){
|
||||
for(int x = 0; x < size; x++){
|
||||
float coverage = fn.get(x, y);
|
||||
pix.setRaw(baseX + x, baseY + y, Color.rgba8888(1f, 1f, 1f, coverage));
|
||||
}
|
||||
}
|
||||
|
||||
page.setDirty(true);
|
||||
|
||||
Texture texture = page.texture;
|
||||
|
||||
//top-right corner tile, exactly as generated
|
||||
TextureRegion topRight = new TextureRegion(texture, baseX, baseY, size, size);
|
||||
TextureRegion topLeft = new TextureRegion(topRight);
|
||||
topLeft.flip(true, false);
|
||||
TextureRegion bottomRight = new TextureRegion(topRight);
|
||||
bottomRight.flip(false, true);
|
||||
TextureRegion bottomLeft = new TextureRegion(topRight);
|
||||
bottomLeft.flip(true, true);
|
||||
|
||||
return new TextureRegion[]{topLeft, topRight, bottomLeft, bottomRight};
|
||||
}
|
||||
|
||||
/** Builds a filled nine-patch drawable out of the four corner tiles returned by {@link #renderCorner}. */
|
||||
private static NinePatchDrawable ninePatch(TextureRegion[] corners){
|
||||
TextureRegion topLeft = corners[0], topRight = corners[1], bottomLeft = corners[2], bottomRight = corners[3];
|
||||
int size = topRight.height;
|
||||
|
||||
//note: Core.atlas uses an array texture, and in this case it is guaranteed to be the same, assuming the packer is set up to target it
|
||||
TextureRegion white = Core.atlas.white();
|
||||
|
||||
TextureRegion top = new TextureRegion(white);
|
||||
top.height = size;
|
||||
TextureRegion bottom = new TextureRegion(white);
|
||||
bottom.height = size;
|
||||
TextureRegion left = new TextureRegion(white);
|
||||
left.width = size;
|
||||
TextureRegion right = new TextureRegion(white);
|
||||
right.width = size;
|
||||
TextureRegion center = new TextureRegion(white);
|
||||
|
||||
NinePatch patch = new NinePatch(
|
||||
topLeft, top, topRight,
|
||||
left, center, right,
|
||||
bottomLeft, bottom, bottomRight
|
||||
);
|
||||
|
||||
return new NinePatchDrawable(patch);
|
||||
}
|
||||
|
||||
/** Builds an {@link OutlinedCornerDrawable} out of the four corner tiles returned by {@link #renderCorner}. */
|
||||
private static OutlinedCornerDrawable outlinedDrawable(TextureRegion[] corners, int size, float stroke){
|
||||
TextureRegion topLeft = corners[0], topRight = corners[1], bottomLeft = corners[2], bottomRight = corners[3];
|
||||
|
||||
//note: Core.atlas uses an array texture, and in this case it is guaranteed to be the same, assuming the packer is set up to target it
|
||||
return new OutlinedCornerDrawable(topLeft, topRight, bottomLeft, bottomRight, Core.atlas.white(), size, stroke);
|
||||
}
|
||||
|
||||
/** @return the antialiased coverage (0-1) of pixel (x, y) with an angled edge. */
|
||||
private static float coverage(int x, int y, int size){
|
||||
int inside = 0;
|
||||
for(int sy = 0; sy < supersample; sy++){
|
||||
float fy = y + (sy + 0.5f) / supersample;
|
||||
for(int sx = 0; sx < supersample; sx++){
|
||||
float fx = x + (sx + 0.5f) / supersample;
|
||||
if(fy > fx) inside++;
|
||||
}
|
||||
}
|
||||
return inside / (float)(supersample * supersample);
|
||||
}
|
||||
|
||||
/** @return the antialiased coverage (0-1) of pixel (x, y) with an angled outlined edge. */
|
||||
private static float outlineCoverage(int x, int y, float stroke){
|
||||
int inside = 0;
|
||||
float band = stroke;
|
||||
float extrude = stroke * ((float)Math.sqrt(2.0) - 1f);
|
||||
for(int sy = 0; sy < supersample; sy++){
|
||||
float fy = y + (sy + 0.5f) / supersample;
|
||||
for(int sx = 0; sx < supersample; sx++){
|
||||
float fx = x + (sx + 0.5f) / supersample;
|
||||
float diff = fy - fx;
|
||||
//on the filled side of the diagonal, extruded a bit toward the top-right, and within the band
|
||||
if(diff >= -extrude && diff < band) inside++;
|
||||
}
|
||||
}
|
||||
return inside / (float)(supersample * supersample);
|
||||
}
|
||||
|
||||
private static float cornerDistance(float fx, float fy, int size){
|
||||
float qy = Math.max(size - fy, 0f);
|
||||
return (float)Math.sqrt(fx * fx + qy * qy) - size;
|
||||
}
|
||||
|
||||
private static float roundCoverage(int x, int y, int size){
|
||||
float d = cornerDistance(x + 0.5f, y + 0.5f, size);
|
||||
return Mathf.clamp(0.5f - d);
|
||||
}
|
||||
|
||||
private static float outlineRoundCoverage(int x, int y, int size, float stroke){
|
||||
float d = cornerDistance(x + 0.5f, y + 0.5f, size);
|
||||
return Mathf.clamp(0.5f - d) - Mathf.clamp(0.5f - d - stroke);
|
||||
}
|
||||
|
||||
/** (x, y) pixel coordinate -> coverage (0-1). */
|
||||
private interface CoverageFn{
|
||||
float get(int x, int y);
|
||||
}
|
||||
}
|
||||
169
core/src/mindustry/graphics/OutlinedCornerDrawable.java
Normal file
169
core/src/mindustry/graphics/OutlinedCornerDrawable.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package mindustry.graphics;
|
||||
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.style.*;
|
||||
|
||||
/**
|
||||
* Drawable for an antialiased, beveled-corner <i>outline</i>, as generated by {@link CornerGenerator#generateAngledOutline(int, float)}.
|
||||
* <p>
|
||||
* This looks similar to a {@link NinePatchDrawable} but it isn't a true nine-patch; the 4 corners are generated, the center is not drawn, and the 4 sides are drawn as lines.
|
||||
*/
|
||||
public class OutlinedCornerDrawable extends BaseDrawable implements TransformDrawable{
|
||||
private static final Color tmpColor = new Color();
|
||||
|
||||
//quad slots within the vertex array; there is deliberately no "center" slot, as it is never drawn
|
||||
private static final int topLeft = 0, topRight = 1, bottomLeft = 2, bottomRight = 3,
|
||||
top = 4, bottom = 5, left = 6, right = 7;
|
||||
private static final int quads = 8;
|
||||
|
||||
private final Color color = new Color(Color.white);
|
||||
private final float[] vertices = new float[quads * SpriteBatch.spriteSize];
|
||||
private Texture texture;
|
||||
|
||||
private float cornerSize, strokeWidth;
|
||||
|
||||
/**
|
||||
* @param topLeftRegion generated corner tile for the top-left corner (flipped horizontally from the base tile)
|
||||
* @param topRightRegion generated corner tile for the top-right corner (the base tile, unflipped)
|
||||
* @param bottomLeftRegion generated corner tile for the bottom-left corner (flipped both ways)
|
||||
* @param bottomRightRegion generated corner tile for the bottom-right corner (flipped vertically)
|
||||
* @param white a plain white region, used to draw the four straight sides; must share a texture with the corners
|
||||
* @param cornerSize size of each (square) corner region, in draw-time pixels
|
||||
* @param strokeWidth thickness of the outline, in draw-time pixels
|
||||
*/
|
||||
public OutlinedCornerDrawable(TextureRegion topLeftRegion, TextureRegion topRightRegion, TextureRegion bottomLeftRegion, TextureRegion bottomRightRegion, TextureRegion white, float cornerSize, float strokeWidth){
|
||||
|
||||
this.texture = white.texture;
|
||||
this.cornerSize = cornerSize;
|
||||
this.strokeWidth = strokeWidth;
|
||||
|
||||
setUv(topLeft, topLeftRegion);
|
||||
setUv(topRight, topRightRegion);
|
||||
setUv(bottomLeft, bottomLeftRegion);
|
||||
setUv(bottomRight, bottomRightRegion);
|
||||
setUv(top, white);
|
||||
setUv(bottom, white);
|
||||
setUv(left, white);
|
||||
setUv(right, white);
|
||||
|
||||
setMinWidth(cornerSize * 2f);
|
||||
setMinHeight(cornerSize * 2f);
|
||||
setTopHeight(cornerSize);
|
||||
setBottomHeight(cornerSize);
|
||||
setLeftWidth(cornerSize);
|
||||
setRightWidth(cornerSize);
|
||||
}
|
||||
|
||||
private void setUv(int quad, TextureRegion region){
|
||||
int idx = quad * SpriteBatch.spriteSize;
|
||||
float u = region.u, v = region.v2, u2 = region.u2, v2 = region.v;
|
||||
float depth = region.getDepth();
|
||||
float[] vs = vertices;
|
||||
|
||||
vs[idx + 2] = u; vs[idx + 3] = v; vs[idx + 4] = depth;
|
||||
vs[idx + 9] = u; vs[idx + 10] = v2; vs[idx + 11] = depth;
|
||||
vs[idx + 16] = u2; vs[idx + 17] = v2; vs[idx + 18] = depth;
|
||||
vs[idx + 23] = u2; vs[idx + 24] = v; vs[idx + 25] = depth;
|
||||
}
|
||||
|
||||
/** Sets the position and color of one of the eight quads. */
|
||||
private void setQuad(int quad, float x, float y, float w, float h, float c){
|
||||
int idx = quad * SpriteBatch.spriteSize;
|
||||
float x2 = x + w, y2 = y + h;
|
||||
float[] vs = vertices;
|
||||
|
||||
vs[idx] = x; vs[idx + 1] = y; vs[idx + 5] = c;
|
||||
vs[idx + 7] = x; vs[idx + 8] = y2; vs[idx + 12] = c;
|
||||
vs[idx + 14] = x2; vs[idx + 15] = y2; vs[idx + 19] = c;
|
||||
vs[idx + 21] = x2; vs[idx + 22] = y; vs[idx + 26] = c;
|
||||
}
|
||||
|
||||
private void prepareVertices(float x, float y, float width, float height){
|
||||
float c = tmpColor.set(color).mul(Draw.getColor()).toFloatBits();
|
||||
|
||||
float innerX = x + cornerSize, innerY = y + cornerSize;
|
||||
float rightX = x + width - cornerSize, topY = y + height - cornerSize;
|
||||
|
||||
//corners: full generated tiles, unscaled, sitting exactly in the four corners
|
||||
setQuad(bottomLeft, x, y, cornerSize, cornerSize, c);
|
||||
setQuad(bottomRight, rightX, y, cornerSize, cornerSize, c);
|
||||
setQuad(topLeft, x, topY, cornerSize, cornerSize, c);
|
||||
setQuad(topRight, rightX, topY, cornerSize, cornerSize, c);
|
||||
|
||||
//sides: plain white, only as thick as the stroke, spanning the gap between corners - not the corner size
|
||||
float sideWidth = Math.max(0f, width - cornerSize * 2f);
|
||||
float sideHeight = Math.max(0f, height - cornerSize * 2f);
|
||||
|
||||
setQuad(bottom, innerX, y, sideWidth, strokeWidth, c);
|
||||
setQuad(top, innerX, y + height - strokeWidth, sideWidth, strokeWidth, c);
|
||||
setQuad(left, x, innerY, strokeWidth, sideHeight, c);
|
||||
setQuad(right, x + width - strokeWidth, innerY, strokeWidth, sideHeight, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(float x, float y, float width, float height){
|
||||
prepareVertices(x, y, width, height);
|
||||
Draw.vert(texture, vertices, 0, vertices.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(float x, float y, float originX, float originY, float width, float height, float scaleX, float scaleY, float rotation){
|
||||
prepareVertices(x, y, width, height);
|
||||
|
||||
float worldOriginX = x + originX, worldOriginY = y + originY;
|
||||
float[] vs = vertices;
|
||||
int n = vs.length;
|
||||
|
||||
if(rotation != 0){
|
||||
float cos = Mathf.cosDeg(rotation), sin = Mathf.sinDeg(rotation);
|
||||
for(int i = 0; i < n; i += SpriteBatch.vertexSize){
|
||||
float vx = (vs[i] - worldOriginX) * scaleX, vy = (vs[i + 1] - worldOriginY) * scaleY;
|
||||
vs[i] = cos * vx - sin * vy + worldOriginX;
|
||||
vs[i + 1] = sin * vx + cos * vy + worldOriginY;
|
||||
}
|
||||
}else if(scaleX != 1f || scaleY != 1f){
|
||||
for(int i = 0; i < n; i += SpriteBatch.vertexSize){
|
||||
vs[i] = (vs[i] - worldOriginX) * scaleX + worldOriginX;
|
||||
vs[i + 1] = (vs[i + 1] - worldOriginY) * scaleY + worldOriginY;
|
||||
}
|
||||
}
|
||||
|
||||
Draw.vert(texture, vs, 0, n);
|
||||
}
|
||||
|
||||
public float getCornerSize(){
|
||||
return cornerSize;
|
||||
}
|
||||
|
||||
public float getStrokeWidth(){
|
||||
return strokeWidth;
|
||||
}
|
||||
|
||||
public Color getColor(){
|
||||
return color;
|
||||
}
|
||||
|
||||
/** Copy given color. Blended with the batch color, then combined with the texture colors at draw time. */
|
||||
public void setColor(Color color){
|
||||
this.color.set(color);
|
||||
}
|
||||
|
||||
/** Creates a new drawable that renders the same as this drawable tinted the specified color. */
|
||||
public OutlinedCornerDrawable tint(Color tint){
|
||||
OutlinedCornerDrawable drawable = new OutlinedCornerDrawable(this);
|
||||
drawable.color.set(tint);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
/** Copy constructor; shares the same texture/UV data (via {@link #vertices}) as the source drawable. */
|
||||
private OutlinedCornerDrawable(OutlinedCornerDrawable other){
|
||||
super(other);
|
||||
this.texture = other.texture;
|
||||
this.cornerSize = other.cornerSize;
|
||||
this.strokeWidth = other.strokeWidth;
|
||||
this.color.set(other.color);
|
||||
System.arraycopy(other.vertices, 0, this.vertices, 0, other.vertices.length);
|
||||
}
|
||||
}
|
||||
@@ -558,7 +558,7 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
//the requiredPlanets filter needs this, since the rules aren't read yet
|
||||
if(headless){
|
||||
try{
|
||||
Planet planet = content.planet(new JsonReader().parse(saveState.ruleString).getString("planet", Planets.serpulo.name));
|
||||
Planet planet = content.planet(Jval.read(saveState.ruleString).getString("planet", Planets.serpulo.name));
|
||||
state.rules.planet = planet == null ? Planets.serpulo : planet;
|
||||
}catch(Exception ignored){}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,4 @@ org.gradle.caching=true
|
||||
org.gradle.internal.http.socketTimeout=100000
|
||||
org.gradle.internal.http.connectionTimeout=100000
|
||||
android.enableR8.fullMode=false
|
||||
archash=b63432df12
|
||||
archash=c082cc548b
|
||||
|
||||
Reference in New Issue
Block a user