Skip to content

Commit

Permalink
feat(Core): Image Border
Browse files Browse the repository at this point in the history
  • Loading branch information
Dituon committed Jan 24, 2025
1 parent 35a4400 commit a53c5dc
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ protected BufferedImage createImage(int length, int i, int width, int height) {
var img = new BufferedImage(width, height, length == 1 ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_3BYTE_BGR);
var g2d = img.createGraphics();
var color = ElementFrame.getNElement(template.getColor(), i);
if (color.getAlpha() != 255) {
if (color.getAlpha() != 0) {
g2d.setColor(color);
g2d.fillRect(0, 0, width, height);
}
Expand Down
18 changes: 9 additions & 9 deletions core/src/main/java/moe/dituon/petpet/core/clip/BorderRadius.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ public Shape getShape(RealPosition position) {


Path2D path = new Path2D.Float();
path.moveTo(leftTopX, 0);
path.lineTo(w - rightTopX, 0);
path.curveTo(w - rightTopX / 2, 0, w, rightTopY / 2, w, rightTopY);
path.lineTo(w, h - rightBottomY);
path.curveTo(w, h - rightBottomY / 2, w - rightBottomX / 2, h, w - rightBottomX, h);
path.lineTo(leftBottomX, h);
path.curveTo(leftBottomX / 2, h, 0, h - leftBottomY / 2, 0, h - leftBottomY);
path.lineTo(0, leftTopY);
path.curveTo(0, leftTopY / 2, leftTopX / 2, 0, leftTopX, 0);
path.moveTo(leftTopX + x, y); // 加入 x 偏移
path.lineTo(w - rightTopX + x, y);
path.curveTo(w - rightTopX / 2 + x, y, w + x, rightTopY / 2 + y, w + x, rightTopY + y);
path.lineTo(w + x, h - rightBottomY + y);
path.curveTo(w + x, h - rightBottomY / 2 + y, w - rightBottomX / 2 + x, h + y, w - rightBottomX + x, h + y);
path.lineTo(leftBottomX + x, h + y);
path.curveTo(leftBottomX / 2 + x, h + y, x, h - leftBottomY / 2 + y, x, h - leftBottomY + y);
path.lineTo(x, leftTopY + y);
path.curveTo(x, leftTopY / 2 + y, leftTopX / 2 + x, y, leftTopX + x, y);
path.closePath();
return path;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ protected Graphics2D createGraphics(BufferedImage image) {
RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);

g2d.setRenderingHint(
RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);

return g2d;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import moe.dituon.petpet.core.position.FitType;
import moe.dituon.petpet.core.transform.Offset;
import moe.dituon.petpet.core.transition.RotateTransition;
import moe.dituon.petpet.core.utils.image.ImageBorder;
import moe.dituon.petpet.core.utils.image.ImageCropper;
import moe.dituon.petpet.template.element.AvatarTemplate;
import org.jetbrains.annotations.Nullable;
Expand All @@ -19,6 +20,7 @@ public abstract class AvatarFrame extends ElementFrame {
@Nullable
public final String defaultUrl;
public final ImageCropper cropper;
public final ImageBorder border;
public final FitType fit;
public final Offset position;
public final float angle;
Expand All @@ -36,6 +38,7 @@ protected AvatarFrame(int index, AvatarTemplate template) {
this.id = getNElement(template.getKey());
this.defaultUrl = getNElement(template.getDefault());
this.cropper = getNElement(template.getCrop());
this.border = getNElement(template.getBorder());
this.fit = getNElement(template.getFit());
this.position = getNElement(template.getPosition());
this.angle = getNElement(template.getAngle());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,16 @@ public void draw(int customIndex) {
g2d.setTransform(transform);
}
Shape prevClip = null;
Shape borderShape = null;
if (AvatarXYWHFrame.super.borderRadius != null) {
prevClip = g2d.getClip();
var shape = AvatarXYWHFrame.super.borderRadius.getShape(
borderShape = AvatarXYWHFrame.super.borderRadius.getShape(
new ClipPath.RealPosition(
lengthContext.canvasWidth, lengthContext.canvasHeight,
x, y, w, h
)
);
g2d.setClip(shape);
g2d.setClip(borderShape);
}

// FEATURE: alpha
Expand Down Expand Up @@ -273,8 +274,17 @@ public void draw(int customIndex) {
default:
g2d.drawImage(img, x, y, w, h, null);
}
if (prevTransform != null) g2d.setTransform(prevTransform);
g2d.setClip(prevClip);
if (prevTransform != null) {
g2d.setTransform(prevTransform);
}
if (borderShape != null) {
AvatarXYWHFrame.super.border.draw(g2d, borderShape, lengthContext);
} else {
AvatarXYWHFrame.super.border.draw(g2d, x, y, w, h);
}
if (AvatarXYWHFrame.super.borderRadius != null) {
g2d.setClip(prevClip);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package moe.dituon.petpet.core.utils.image;

import moe.dituon.petpet.core.length.Length;
import moe.dituon.petpet.core.length.LengthContext;
import moe.dituon.petpet.core.length.NumberLength;
import moe.dituon.petpet.template.fields.ColorKt;
import org.jetbrains.annotations.NotNull;

import java.awt.*;

public class ImageBorder {
public final Length length;
public final Color color;
protected final boolean isAbsolute;
protected final BasicStroke staticStroke;

public ImageBorder(@NotNull Length length, Color color) {
this.length = length;
this.color = color;
this.isAbsolute = length.isAbsolute();
this.staticStroke = isAbsolute ? new BasicStroke(length.getValue()) : null;
}

public void draw(Graphics2D g2d, Shape borderShape) {
draw(g2d, borderShape, null);
}

public void draw(Graphics2D g2d, Shape borderShape, LengthContext context) {
g2d.setColor(color);
if (isAbsolute) {
g2d.setStroke(staticStroke);
} else if (context == null) {
throw new IllegalArgumentException("context must not be null");
} else {
g2d.setStroke(new BasicStroke(length.getValue(context)));
}
if (staticStroke.getLineWidth() <= 0f) return;
g2d.draw(borderShape);
}

public void draw(Graphics2D g2d, int x, int y, int width, int height) {
g2d.setColor(color);
g2d.setStroke(staticStroke);
g2d.drawRect(x, y, width, height);
}

public static class EmptyImageBorder extends ImageBorder {
public static final EmptyImageBorder INSTANCE = new EmptyImageBorder();

protected EmptyImageBorder() {
super(NumberLength.EMPTY, null);
}

@Override
public void draw(Graphics2D g2d, Shape borderShape) {
}

@Override
public void draw(Graphics2D g2d, Shape borderShape, LengthContext context) {
}
}

public static ImageBorder empty() {
return EmptyImageBorder.INSTANCE;
}

public static ImageBorder fromString(String str) {
var parts = Length.splitString(str);
Color color = Color.BLACK;
Length length = null;
switch (parts.size()) {
case 1:
if (parts.get(0).startsWith("#")) {
color = ColorKt.decodeColor(parts.get(0));
} else {
length = Length.fromString(parts.get(0));
}
break;
case 2:
if (parts.get(0).startsWith("#")) {
color = ColorKt.decodeColor(parts.get(0));
length = Length.fromString(parts.get(1));
} else {
length = Length.fromString(parts.get(0));
color = ColorKt.decodeColor(parts.get(1));
}
break;
default:
throw new IllegalArgumentException("border must has 1 or 2 argument");
}
if (length == null || (length.isAbsolute() && length.getValue() <= 0f)) {
return EmptyImageBorder.INSTANCE;
}
return new ImageBorder(length, color);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import moe.dituon.petpet.core.position.AvatarCoords
import moe.dituon.petpet.core.position.AvatarXYWHCoords
import moe.dituon.petpet.core.position.FitType
import moe.dituon.petpet.core.transform.Offset
import moe.dituon.petpet.core.utils.image.ImageBorder
import moe.dituon.petpet.core.utils.image.ImageCropper
import moe.dituon.petpet.core.utils.image.ImageXYWHCropper
import moe.dituon.petpet.template.fields.BorderList
import moe.dituon.petpet.template.fields.ImageFilterListElement
import moe.dituon.petpet.template.fields.ImageFilterTemplate
import moe.dituon.petpet.template.fields.length.*
Expand Down Expand Up @@ -54,6 +56,7 @@ data class AvatarTemplate(
val rotate: RotateTransitionElement? = null,
val origin: OffsetList = listOf(Offset.CENTER),
val opacity: FloatOrArray = floatArrayOf(1f),
val border: BorderList = listOf(ImageBorder.EmptyImageBorder.INSTANCE),
@JsonNames("border_radius")
val borderRadius: BorderRadiusList = emptyList(),
val filter: ImageFilterListElement = ImageFilterList.EMPTY,
Expand All @@ -72,6 +75,7 @@ data class AvatarTemplate(
angle.size,
origin.size,
opacity.size,
border.size,
borderRadius.size,
if (filter.isEmpty()) 0 else filter.maxOf { it.maxLength }
)
Expand All @@ -87,6 +91,7 @@ data class AvatarTemplate(
angle = builder.angle,
origin = builder.origin,
opacity = builder.opacity,
border = builder.border,
borderRadius = builder.borderRadius,
filter = builder.filter,
)
Expand Down Expand Up @@ -137,6 +142,8 @@ data class AvatarTemplate(
private set
var opacity: FloatArray = floatArrayOf(1f)
private set
var border: List<ImageBorder> = listOf(ImageBorder.EmptyImageBorder.INSTANCE)
private set
var borderRadius: List<BorderRadius> = emptyList()
private set
var filter: ImageFilterList = ImageFilterList.EMPTY
Expand Down Expand Up @@ -174,6 +181,9 @@ data class AvatarTemplate(
fun opacity(opacity: Float) = apply { this.opacity = floatArrayOf(opacity) }
fun opacity(opacity: FloatArray) = apply { this.opacity = opacity }

fun border(border: ImageBorder) = apply { this.border = listOf(border) }
fun border(border: List<ImageBorder>) = apply { this.border = border }

fun borderRadius(borderRadius: BorderRadius) = apply { this.borderRadius = listOf(borderRadius) }
fun borderRadius(borderRadius: List<BorderRadius>) = apply { this.borderRadius = borderRadius }

Expand Down
31 changes: 31 additions & 0 deletions core/src/main/kotlin/moe/dituon/petpet/template/fields/Border.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package moe.dituon.petpet.template.fields

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonTransformingSerializer
import moe.dituon.petpet.core.utils.image.ImageBorder
import moe.dituon.petpet.uitls.wrapJsonElement

object BorderSerializer : KSerializer<ImageBorder> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Border", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: ImageBorder) =
encoder.encodeString(value.toString())

override fun deserialize(decoder: Decoder): ImageBorder =
ImageBorder.fromString(decoder.decodeString())
}

object BorderListSerializer : JsonTransformingSerializer<List<ImageBorder>>(ListSerializer(BorderSerializer)) {
override fun transformDeserialize(element: JsonElement) = wrapJsonElement(element)
}

typealias BorderElement = @Serializable(BorderSerializer::class) ImageBorder
typealias BorderList = @Serializable(BorderListSerializer::class) List<BorderElement>
14 changes: 14 additions & 0 deletions docs/template/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,22 @@ P0 为 (0, 0),表示初始进度和初始状态。P3 为 (1, 1),表示最终

![origin](../images/origin.png)

## Border

> 目前仅在坐标格式为 `x, y, width, height` 时有效。
`"<width> <color>"`

例如:

- `"6px #ffffff"`: 6px 宽度,白色边框
- `"#ffffff 6px"`: 同上
- `"2px"`: 2px 宽度,默认颜色 (`#000000`) 边框

## BorderRadius

> 目前仅在坐标格式为 `x, y, width, height` 时有效。
`"left-top right-top left-bottom right-bottom"`

简写形式:
Expand Down

0 comments on commit a53c5dc

Please sign in to comment.