StyleRenderer.java
package swingtree.style;
import com.github.weisj.jsvg.geometry.size.FloatSize;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import swingtree.UI;
import swingtree.api.Painter;
import swingtree.layout.Bounds;
import swingtree.layout.Size;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
/**
* A stateless un-instantiable utility class that renders the style of a component
* using the immutable {@link LayerRenderConf} object containing the essential state
* needed for rendering, like for example the current {@link Bounds} and {@link StyleConf}
* of a particular component.
*/
final class StyleRenderer
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(StyleRenderer.class);
private StyleRenderer() {} // Un-instantiable!
public static void renderStyleOn(
UI.Layer layer,
LayerRenderConf conf,
Graphics2D g2d
) {
// First we render things unique to certain layers:
// Background stuff:
conf.baseColors().foundationColor().ifPresent(outerColor -> {
if ( outerColor.getAlpha() > 0 ) { // Avoid rendering a fully transparent color!
g2d.setColor(outerColor);
g2d.fill(conf.areas().get(UI.ComponentArea.EXTERIOR));
}
});
conf.baseColors().backgroundColor().ifPresent(color -> {
if ( color.getAlpha() > 0 ) { // Avoid rendering a fully transparent color!
g2d.setColor(color);
g2d.fill(conf.areas().get(UI.ComponentArea.BODY));
}
});
// Border stuff:
_drawBorder( conf, conf.baseColors().borderColor(), g2d);
// Now onto things every layer has in common:
// Every layer has 4 things:
// 1. A grounding serving as a base background, which is a filled color and/or an image:
for ( ImageConf imageConf : conf.layer().images().sortedByNames() )
if ( !imageConf.equals(ImageConf.none()) )
_renderImage( conf, imageConf, conf.boxModel().size(), g2d);
// 2. Gradients, which are best used to give a component a nice surface lighting effect.
// They may transition vertically, horizontally or diagonally over various different colors:
for ( GradientConf gradient : conf.layer().gradients().sortedByNames() )
if ( gradient.colors().length > 0 ) {
_renderGradient( gradient, conf, g2d );
}
// 3. Noise, which is a simple way to add a bit of texture to a component:
for ( NoiseConf noise : conf.layer().noises().sortedByNames() )
if ( noise.colors().length > 0 ) {
_renderNoise( noise, conf, g2d );
}
// 4. Shadows, which are simple gradient based drop shadows that can go inwards or outwards
for ( ShadowConf shadow : conf.layer().shadows().sortedByNames() )
_renderShadows(conf, shadow, g2d);
// 5. Custom text, which can be rendered in any font and color:
for ( TextConf text : conf.layer().texts().sortedByNames() )
_renderText( text, conf, g2d );
// 6. Painters, which are provided by the user and can be anything
List<PainterConf> painters = conf.layer().painters().sortedByNames();
if ( !painters.isEmpty() )
{
for ( PainterConf painterConf : painters )
{
Painter backgroundPainter = painterConf.painter();
if ( backgroundPainter == Painter.none() )
continue;
Shape allowedArea = conf.areas().get(painterConf.clipArea());
_paintClippedTo( allowedArea, g2d, () -> {
// We remember the current transform and clip so that we can reset them after each painter:
AffineTransform currentTransform = new AffineTransform(g2d.getTransform());
Shape currentClip = g2d.getClip();
// We remember if antialiasing was enabled before we render:
boolean antialiasingWasEnabled = g2d.getRenderingHint( RenderingHints.KEY_ANTIALIASING ) == RenderingHints.VALUE_ANTIALIAS_ON;
try {
backgroundPainter.paint(g2d);
} catch (Exception e) {
log.warn(
"An exception occurred while executing painter '" + backgroundPainter + "' " +
"on layer '" + layer + "' for style '" + conf + "' ",
e
);
/*
If exceptions happen in user provided painters, we don't want to
mess up the rendering of the rest of the component, so we catch them here!
We log as warning because exceptions during rendering are not considered
as harmful as elsewhere!
Hi there! If you are reading this, you are probably a developer using the SwingTree
library, thank you for using it! Good luck finding out what went wrong! :)
*/
} finally {
// We do not know what the painter did to the graphics object, so we reset it:
g2d.setTransform(currentTransform);
g2d.setClip(currentClip);
// Reset antialiasing to its previous state:
g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, antialiasingWasEnabled ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF );
}
});
}
}
// And that's it! We have rendered a style layer!
}
private static void _paintClippedTo( @Nullable Shape newClip, Graphics g, Runnable painter ) {
Shape oldClip = g.getClip();
if ( newClip != null && newClip != oldClip ) {
newClip = StyleUtil.intersect(newClip, oldClip);
g.setClip(newClip);
}
painter.run();
g.setClip(oldClip);
}
private static void _drawBorder( LayerRenderConf conf, BorderColorsConf colors, Graphics2D g2d )
{
if ( colors.equals(BorderColorsConf.none()) )
return;
if ( !Outline.none().equals(conf.boxModel().widths()) ) {
try {
Area borderArea = conf.areas().get(UI.ComponentArea.BORDER);
Objects.requireNonNull(borderArea);
if ( colors.isHomogeneous() ) {
g2d.setColor(colors.bottom().orElse(UI.Color.BLACK));
g2d.fill(borderArea);
} else {
Area[] borderEdgeRegions = conf.areas().getEdgeAreas();
// We created clipped border areas:
Area topBorderArea = new Area(borderArea);
topBorderArea.intersect(borderEdgeRegions[0]);
Area rightBorderArea = new Area(borderArea);
rightBorderArea.intersect(borderEdgeRegions[1]);
Area bottomBorderArea = new Area(borderArea);
bottomBorderArea.intersect(borderEdgeRegions[2]);
Area leftBorderArea = new Area(borderArea);
leftBorderArea.intersect(borderEdgeRegions[3]);
// Now we can draw the borders:
g2d.setColor(colors.top().orElse(UI.Color.BLACK));
g2d.fill(topBorderArea);
g2d.setColor(colors.right().orElse(UI.Color.BLACK));
g2d.fill(rightBorderArea);
g2d.setColor(colors.bottom().orElse(UI.Color.BLACK));
g2d.fill(bottomBorderArea);
g2d.setColor(colors.left().orElse(UI.Color.BLACK));
g2d.fill(leftBorderArea);
}
} catch ( Exception e ) {
log.warn(
"An exception occurred while drawing the border of border style '" + conf.boxModel() + "' ",
e
);
/*
If exceptions happen in user provided painters, we don't want to
mess up the rendering of the rest of the component, so we catch them here!
*/
}
}
}
private static void _renderShadows(
LayerRenderConf conf,
ShadowConf shadow,
Graphics2D g2d
) {
if ( !shadow.color().isPresent() )
return;
Color shadowColor = shadow.color().orElse(Color.BLACK);
Size size = conf.boxModel().size();
// First let's check if we need to render any shadows at all
// Is the shadow color transparent?
if ( shadowColor.getAlpha() == 0 )
return;
// The background box is calculated from the margins and border radius:
final float leftBorderWidth = conf.boxModel().widths().left().orElse(0f);
final float topBorderWidth = conf.boxModel().widths().top().orElse(0f);
final float rightBorderWidth = conf.boxModel().widths().right().orElse(0f);
final float bottomBorderWidth = conf.boxModel().widths().bottom().orElse(0f);
final float left = Math.max(conf.boxModel().margin().left().orElse(0f), 0) + ( shadow.isInset() ? leftBorderWidth : 0 );
final float top = Math.max(conf.boxModel().margin().top().orElse(0f), 0) + ( shadow.isInset() ? topBorderWidth : 0 );
final float right = Math.max(conf.boxModel().margin().right().orElse(0f), 0) + ( shadow.isInset() ? rightBorderWidth : 0 );
final float bottom = Math.max(conf.boxModel().margin().bottom().orElse(0f), 0) + ( shadow.isInset() ? bottomBorderWidth : 0 );
final float topLeftRadius = Math.max(conf.boxModel().topLeftRadius(), 0);
final float topRightRadius = Math.max(conf.boxModel().topRightRadius(), 0);
final float bottomRightRadius = Math.max(conf.boxModel().bottomRightRadius(), 0);
final float bottomLeftRadius = Math.max(conf.boxModel().bottomLeftRadius(), 0);
final float width = size.width().orElse(0f);
final float height = size.height().orElse(0f);
// Calculate the shadow box bounds based on the padding and border thickness
final float x = left + shadow.horizontalOffset();
final float y = top + shadow.verticalOffset();
final float w = width - left - right;
final float h = height - top - bottom;
final float blurRadius = Math.max(shadow.blurRadius(), 0);
final float spreadRadius = !shadow.isOutset() ? shadow.spreadRadius() : -shadow.spreadRadius();
Rectangle2D.Float outerShadowRect = new Rectangle2D.Float(
x - blurRadius + spreadRadius,
y - blurRadius + spreadRadius,
w + blurRadius * 2 - spreadRadius * 2,
h + blurRadius * 2 - spreadRadius * 2
);
Function<Integer, Integer> offsetFunction = (radius) -> (int)((radius * 2) / ( shadow.isInset() ? 4.5 : 3.79) );
final int averageCornerRadius = ((int) ( topLeftRadius + topRightRadius + bottomRightRadius + bottomLeftRadius )) / 4;
final int averageBorderWidth = (int) (( leftBorderWidth + topBorderWidth + rightBorderWidth + bottomBorderWidth ) / 4);
final int shadowCornerRadius = (int) Math.max( 0, averageCornerRadius + (shadow.isOutset() ? -spreadRadius-blurRadius*2 : -Math.max(averageBorderWidth,spreadRadius)) );
final int gradientStartOffset = 1 + offsetFunction.apply(shadowCornerRadius);
Rectangle2D.Float innerShadowRect = new Rectangle2D.Float(
x + blurRadius + gradientStartOffset + spreadRadius,
y + blurRadius + gradientStartOffset + spreadRadius,
w - blurRadius * 2 - gradientStartOffset * 2 - spreadRadius * 2,
h - blurRadius * 2 - gradientStartOffset * 2 - spreadRadius * 2
);
final Area baseArea;
if ( shadow.isOutset() ) {
int artifactAdjustment = 1;
baseArea = ComponentAreas.calculateComponentBodyArea(conf.boxModel(), artifactAdjustment, artifactAdjustment, artifactAdjustment, artifactAdjustment);
}
else
baseArea = new Area(conf.areas().get(UI.ComponentArea.BODY));
// Apply the clipping to avoid overlapping the shadow and the box
Area shadowArea = new Area(outerShadowRect);
if ( shadow.isOutset() )
shadowArea.subtract(baseArea);
else
shadowArea.intersect(baseArea);
// Draw the corner shadows
_renderCornerShadow(shadow, UI.Corner.TOP_LEFT, shadowArea, innerShadowRect, outerShadowRect, gradientStartOffset, g2d);
_renderCornerShadow(shadow, UI.Corner.TOP_RIGHT, shadowArea, innerShadowRect, outerShadowRect, gradientStartOffset, g2d);
_renderCornerShadow(shadow, UI.Corner.BOTTOM_LEFT, shadowArea, innerShadowRect, outerShadowRect, gradientStartOffset, g2d);
_renderCornerShadow(shadow, UI.Corner.BOTTOM_RIGHT, shadowArea, innerShadowRect, outerShadowRect, gradientStartOffset, g2d);
// Draw the edge shadows
_renderEdgeShadow(shadow, UI.Edge.TOP, shadowArea, innerShadowRect, outerShadowRect, gradientStartOffset, g2d);
_renderEdgeShadow(shadow, UI.Edge.RIGHT, shadowArea, innerShadowRect, outerShadowRect, gradientStartOffset, g2d);
_renderEdgeShadow(shadow, UI.Edge.BOTTOM, shadowArea, innerShadowRect, outerShadowRect, gradientStartOffset, g2d);
_renderEdgeShadow(shadow, UI.Edge.LEFT, shadowArea, innerShadowRect, outerShadowRect, gradientStartOffset, g2d);
Area outerMostArea = new Area(outerShadowRect);
// If the base rectangle and the outer shadow box are not equal, then we need to fill the area of the base rectangle that is not covered by the outer shadow box!
_renderShadowBody(shadow, baseArea, innerShadowRect, outerMostArea, g2d);
}
private static void _renderShadowBody(
ShadowConf shadowConf,
Area baseArea,
Rectangle2D.Float innerShadowRect,
Area outerShadowBox,
Graphics2D g2d
) {
Graphics2D g2d2 = (Graphics2D) g2d.create();
g2d2.setColor(shadowConf.color().orElse(Color.BLACK));
if ( !shadowConf.isOutset() ) {
baseArea.subtract(outerShadowBox);
g2d2.fill(baseArea);
} else {
Area innerShadowArea = new Area(innerShadowRect);
innerShadowArea.subtract(baseArea);
g2d2.fill(innerShadowArea);
}
g2d2.dispose();
}
private static void _renderCornerShadow(
ShadowConf shadowConf,
UI.Corner corner,
Area areaWhereShadowIsAllowed,
Rectangle2D.Float innerShadowRect,
Rectangle2D.Float outerShadowRect,
int gradientStartOffset,
Graphics2D g2d
) {
// We define a clipping box so that corners don't overlap
float clipBoxWidth = outerShadowRect.width / 2f;
float clipBoxHeight = outerShadowRect.height / 2f;
float clipBoxCenterX = outerShadowRect.x + clipBoxWidth;
float clipBoxCenterY = outerShadowRect.y + clipBoxHeight;
Rectangle2D.Float cornerClipBox; // outer box!
// The defining the corner shadow bound (where it starts and ends
Rectangle2D.Float cornerBox;
float cx;
float cy;
float cr; // depending on the corner, this is either the corner box width or height
switch (corner) {
case TOP_LEFT:
cornerBox = new Rectangle2D.Float(
outerShadowRect.x, outerShadowRect.y,
innerShadowRect.x - outerShadowRect.x,
innerShadowRect.y - outerShadowRect.y
);
cornerClipBox = new Rectangle2D.Float(
clipBoxCenterX - clipBoxWidth, clipBoxCenterY - clipBoxHeight,
clipBoxWidth, clipBoxHeight
);
cx = cornerBox.x + cornerBox.width;
cy = cornerBox.y + cornerBox.height;
cr = cornerBox.width;
break;
case TOP_RIGHT:
cornerBox = new Rectangle2D.Float(
innerShadowRect.x + innerShadowRect.width, outerShadowRect.y,
outerShadowRect.x + outerShadowRect.width - innerShadowRect.x - innerShadowRect.width,
innerShadowRect.y - outerShadowRect.y
);
cornerClipBox = new Rectangle2D.Float(
clipBoxCenterX, clipBoxCenterY - clipBoxHeight,
clipBoxWidth, clipBoxHeight
);
cx = cornerBox.x;
cy = cornerBox.y + cornerBox.height;
cr = cornerBox.width;
break;
case BOTTOM_LEFT:
cornerBox = new Rectangle2D.Float(
outerShadowRect.x,
innerShadowRect.y + innerShadowRect.height,
innerShadowRect.x - outerShadowRect.x,
outerShadowRect.y + outerShadowRect.height - innerShadowRect.y - innerShadowRect.height
);
cornerClipBox = new Rectangle2D.Float(
clipBoxCenterX - clipBoxWidth, clipBoxCenterY,
clipBoxWidth, clipBoxHeight
);
cx = cornerBox.x + cornerBox.width;
cy = cornerBox.y;
cr = cornerBox.width;
break;
case BOTTOM_RIGHT:
cornerBox = new Rectangle2D.Float(
innerShadowRect.x + innerShadowRect.width, innerShadowRect.y + innerShadowRect.height,
outerShadowRect.x + outerShadowRect.width - innerShadowRect.x - innerShadowRect.width,
outerShadowRect.y + outerShadowRect.height - innerShadowRect.y - innerShadowRect.height
);
cornerClipBox = new Rectangle2D.Float(
clipBoxCenterX, clipBoxCenterY,
clipBoxWidth, clipBoxHeight
);
cx = cornerBox.x;
cy = cornerBox.y;
cr = cornerBox.width;
break;
default:
throw new IllegalArgumentException("Invalid corner: " + corner);
}
if (cr <= 0) return;
Color innerColor;
Color outerColor;
Color shadowBackgroundColor = _transparentShadowBackground(shadowConf);
if ( shadowConf.isOutset() ) {
innerColor = shadowConf.color().orElse(Color.BLACK);
outerColor = shadowBackgroundColor;
} else {
innerColor = shadowBackgroundColor;
outerColor = shadowConf.color().orElse(Color.BLACK);
}
float gradientStart = (float) gradientStartOffset / cr;
// The first thing we can do is to clip the corner box to the area where the shadow is allowed
Area cornerArea = new Area(cornerBox);
cornerArea.intersect(areaWhereShadowIsAllowed);
// In the simplest case we don't need to do any gradient painting:
if ( gradientStart == 1f || gradientStart == 0f ) {
// Simple, we just draw a circle and clip it
Area circle = new Area(new Ellipse2D.Float(cx - cr, cy - cr, cr * 2, cr * 2));
if ( shadowConf.isInset() ) {
g2d.setColor(outerColor);
cornerArea.subtract(circle);
} else {
g2d.setColor(innerColor);
cornerArea.intersect(circle);
}
g2d.fill(cornerArea);
return;
}
RadialGradientPaint cornerPaint;
if ( gradientStart > 1f || gradientStart < 0f )
cornerPaint = new RadialGradientPaint(
cx, cy, cr,
new float[] {0f, 1f},
new Color[] {innerColor, outerColor}
);
else
cornerPaint = new RadialGradientPaint(
cx, cy, cr,
new float[] {0f, gradientStart, 1f},
new Color[] {innerColor, innerColor, outerColor}
);
// We need to clip the corner paint to the corner box
cornerArea.intersect(new Area(cornerClipBox));
Graphics2D cornerG2d = (Graphics2D) g2d.create();
cornerG2d.setPaint(cornerPaint);
cornerG2d.fill(cornerArea);
cornerG2d.dispose();
}
private static void _renderEdgeShadow(
ShadowConf shadowConf,
UI.Edge edge,
Area contentArea,
Rectangle2D.Float innerShadowRect,
Rectangle2D.Float outerShadowRect,
int gradientStartOffset,
Graphics2D g2d
) {
// We define a boundary center point and a clipping box so that edges don't overlap
float clipBoundaryX = outerShadowRect.x + outerShadowRect.width / 2f;
float clipBoundaryY = outerShadowRect.y + outerShadowRect.height / 2f;
Rectangle2D.Float edgeClipBox = null;
Rectangle2D.Float edgeBox;
float gradEndX;
float gradEndY;
float gradStartX;
float gradStartY;
switch (edge) {
case TOP:
edgeBox = new Rectangle2D.Float(
innerShadowRect.x, outerShadowRect.y,
innerShadowRect.width, innerShadowRect.y - outerShadowRect.y
);
if ( (edgeBox.y + edgeBox.height) > clipBoundaryY )
edgeClipBox = new Rectangle2D.Float(
edgeBox.x, edgeBox.y,
edgeBox.width, clipBoundaryY - edgeBox.y
);
gradEndX = edgeBox.x;
gradEndY = edgeBox.y;
gradStartX = edgeBox.x;
gradStartY = edgeBox.y + edgeBox.height;
break;
case RIGHT:
edgeBox = new Rectangle2D.Float(
innerShadowRect.x + innerShadowRect.width, innerShadowRect.y,
outerShadowRect.x + outerShadowRect.width - innerShadowRect.x - innerShadowRect.width,
innerShadowRect.height
);
if ( edgeBox.x < clipBoundaryX )
edgeClipBox = new Rectangle2D.Float(
clipBoundaryX, edgeBox.y,
edgeBox.x + edgeBox.width - clipBoundaryX, edgeBox.height
);
gradEndX = edgeBox.x + edgeBox.width;
gradEndY = edgeBox.y;
gradStartX = edgeBox.x;
gradStartY = edgeBox.y;
break;
case BOTTOM:
edgeBox = new Rectangle2D.Float(
innerShadowRect.x, innerShadowRect.y + innerShadowRect.height,
innerShadowRect.width, outerShadowRect.y + outerShadowRect.height - innerShadowRect.y - innerShadowRect.height
);
if ( edgeBox.y < clipBoundaryY )
edgeClipBox = new Rectangle2D.Float(
edgeBox.x,
clipBoundaryY,
edgeBox.width,
edgeBox.y + edgeBox.height - clipBoundaryY
);
gradEndX = edgeBox.x;
gradEndY = edgeBox.y + edgeBox.height;
gradStartX = edgeBox.x;
gradStartY = edgeBox.y;
break;
case LEFT:
edgeBox = new Rectangle2D.Float(
outerShadowRect.x,
innerShadowRect.y,
innerShadowRect.x - outerShadowRect.x,
innerShadowRect.height
);
if ( (edgeBox.x + edgeBox.width) > clipBoundaryX )
edgeClipBox = new Rectangle2D.Float(
edgeBox.x,
edgeBox.y,
clipBoundaryX - edgeBox.x,
edgeBox.height
);
gradEndX = edgeBox.x;
gradEndY = edgeBox.y;
gradStartX = edgeBox.x + edgeBox.width;
gradStartY = edgeBox.y;
break;
default:
throw new IllegalArgumentException("Invalid edge: " + edge);
}
if ( gradStartX == gradEndX && gradStartY == gradEndY ) return;
Color innerColor;
Color outerColor;
// Same as shadow color but without alpha:
Color shadowBackgroundColor = _transparentShadowBackground(shadowConf);
if (shadowConf.isOutset()) {
innerColor = shadowConf.color().orElse(Color.BLACK);
outerColor = shadowBackgroundColor;
} else {
innerColor = shadowBackgroundColor;
outerColor = shadowConf.color().orElse(Color.BLACK);
}
LinearGradientPaint edgePaint;
// distance between start and end of gradient
float dist = (float) Math.sqrt(
(gradEndX - gradStartX) * (gradEndX - gradStartX) +
(gradEndY - gradStartY) * (gradEndY - gradStartY)
);
float gradientStart = (float) gradientStartOffset / dist;
if ( gradientStart > 1f || gradientStart < 0f )
edgePaint = new LinearGradientPaint(
gradStartX, gradStartY,
gradEndX, gradEndY,
new float[] {0f, 1f},
new Color[] {innerColor, outerColor}
);
else {
if ( gradientStart == 1f || gradientStart == 0f ) {
// The gradient does not really exist, so we can just fill the whole area and then return
Area edgeArea = new Area(edgeBox);
g2d.setColor(innerColor);
if ( shadowConf.isOutset() )
edgeArea.intersect(contentArea);
g2d.fill(edgeArea);
return;
}
edgePaint = new LinearGradientPaint(
gradStartX, gradStartY,
gradEndX, gradEndY,
new float[] {0f, gradientStart, 1f},
new Color[] {innerColor, innerColor, outerColor}
);
}
// We need to clip the edge paint to the edge box
Area edgeArea = new Area(edgeBox);
edgeArea.intersect(contentArea);
if ( edgeClipBox != null )
edgeArea.intersect(new Area(edgeClipBox));
Graphics2D edgeG2d = (Graphics2D) g2d.create();
edgeG2d.setPaint(edgePaint);
edgeG2d.fill(edgeArea);
edgeG2d.dispose();
}
private static Color _transparentShadowBackground(ShadowConf shadow) {
return shadow.color()
.map(c -> new Color(c.getRed(), c.getGreen(), c.getBlue(), 0))
.orElse(new Color(0.5f, 0.5f, 0.5f, 0f));
}
private static Outline _insetsFrom(UI.ComponentBoundary boundary, BoxModelConf boxModel) {
Outline insets = Outline.none();
switch ( boundary ) {
case OUTER_TO_EXTERIOR:
insets = Outline.none(); break;
case EXTERIOR_TO_BORDER:
insets = boxModel.margin(); break;
case BORDER_TO_INTERIOR:
insets = boxModel.margin().plus(boxModel.widths()); break;
case INTERIOR_TO_CONTENT:
insets = boxModel.margin().plus(boxModel.widths()).plus(boxModel.padding()); break;
case CENTER_TO_CONTENT:
insets = boxModel.margin().plus(boxModel.widths()).plus(boxModel.padding());
float deltaWidth = boxModel.size().width().orElse(0f) - boxModel.margin().left().orElse(0f) - boxModel.margin().right().orElse(0f);
float deltaHeight = boxModel.size().height().orElse(0f) - boxModel.margin().top().orElse(0f) - boxModel.margin().bottom().orElse(0f);
float halfWidth = deltaWidth / 2f;
float halfHeight = deltaHeight / 2f;
insets = insets.plus(Outline.of(halfHeight, halfWidth, halfHeight, halfWidth));
break;
}
return insets;
}
private static void _renderGradient(
final GradientConf gradient,
final LayerRenderConf conf,
final Graphics2D g2d
) {
if ( gradient.colors().length == 1 ) {
g2d.setColor(gradient.colors()[0]);
g2d.fill(conf.areas().get(gradient.area()));
}
else {
Paint paint = _createGradientPaint(conf.boxModel(), gradient);
if ( paint != null ) {
Area areaToFill = conf.areas().get(gradient.area());
g2d.setPaint(paint);
g2d.fill(areaToFill);
}
}
}
static @Nullable Paint _createGradientPaint(
BoxModelConf boxModel,
GradientConf gradient
) {
final Size dimensions = boxModel.size();
Outline insets;
if ( gradient.boundary() == UI.ComponentBoundary.CENTER_TO_CONTENT ) {
Outline contentIns = _insetsFrom(UI.ComponentBoundary.INTERIOR_TO_CONTENT, boxModel);
float verticalInset = dimensions.height().orElse(0f) / 2f;
float horizontalInset = dimensions.width().orElse(0f) / 2f;
insets = Outline.of(verticalInset, horizontalInset);
switch ( gradient.span() ) {
case TOP_TO_BOTTOM:
insets = insets.withBottom(contentIns.bottom().orElse(0f));
break;
case BOTTOM_TO_TOP:
insets = insets.withTop(contentIns.top().orElse(0f));
break;
case LEFT_TO_RIGHT:
insets = insets.withRight(contentIns.right().orElse(0f));
break;
case RIGHT_TO_LEFT:
insets = insets.withLeft(contentIns.left().orElse(0f));
break;
case TOP_LEFT_TO_BOTTOM_RIGHT:
insets = insets.withBottom(contentIns.bottom().orElse(0f))
.withRight(contentIns.right().orElse(0f));
break;
case BOTTOM_RIGHT_TO_TOP_LEFT:
insets = insets.withTop(contentIns.top().orElse(0f))
.withLeft(contentIns.left().orElse(0f));
break;
case TOP_RIGHT_TO_BOTTOM_LEFT:
insets = insets.withBottom(contentIns.bottom().orElse(0f))
.withLeft(contentIns.left().orElse(0f));
break;
case BOTTOM_LEFT_TO_TOP_RIGHT:
insets = insets.withTop(contentIns.top().orElse(0f))
.withRight(contentIns.right().orElse(0f));
break;
default:
break;
}
} else {
insets = _insetsFrom(gradient.boundary(), boxModel);
}
final float width = dimensions.width().orElse(0f) - ( insets.right().orElse(0f) + insets.left().orElse(0f) );
final float height = dimensions.height().orElse(0f) - ( insets.bottom().orElse(0f) + insets.top().orElse(0f) );
final float realX = insets.left().orElse(0f) + gradient.offset().x();
final float realY = insets.top().orElse(0f) + gradient.offset().y();
Point2D.Float corner1;
Point2D.Float corner2;
final UI.Span type = gradient.span();
if ( type.isOneOf(UI.Span.TOP_LEFT_TO_BOTTOM_RIGHT) ) {
corner1 = new Point2D.Float(realX, realY);
corner2 = new Point2D.Float(realX + width, realY + height);
} else if ( type.isOneOf(UI.Span.BOTTOM_LEFT_TO_TOP_RIGHT) ) {
corner1 = new Point2D.Float(realX, realY + height);
corner2 = new Point2D.Float(realX + width, realY);
} else if ( type.isOneOf(UI.Span.TOP_RIGHT_TO_BOTTOM_LEFT) ) {
corner1 = new Point2D.Float(realX + width, realY);
corner2 = new Point2D.Float(realX, realY + height);
} else if ( type.isOneOf(UI.Span.BOTTOM_RIGHT_TO_TOP_LEFT) ) {
corner1 = new Point2D.Float(realX + width, realY + height);
corner2 = new Point2D.Float(realX, realY);
} else if ( type == UI.Span.TOP_TO_BOTTOM ) {
corner1 = new Point2D.Float(realX, realY);
corner2 = new Point2D.Float(realX, realY + height);
} else if ( type == UI.Span.LEFT_TO_RIGHT ) {
corner1 = new Point2D.Float(realX, realY);
corner2 = new Point2D.Float(realX + width, realY);
} else if ( type == UI.Span.BOTTOM_TO_TOP ) {
corner1 = new Point2D.Float(realX, realY + height);
corner2 = new Point2D.Float(realX, realY);
} else if ( type == UI.Span.RIGHT_TO_LEFT ) {
corner1 = new Point2D.Float(realX + width, realY);
corner2 = new Point2D.Float(realX, realY);
}
else {
log.warn("Unknown gradient type: " + type, new Throwable());
return null;
}
if ( gradient.type() == UI.GradientType.CONIC )
return _createConicGradientPaint(corner1, corner2, gradient);
else if ( gradient.type() == UI.GradientType.RADIAL )
return _createRadialGradientPaint(corner1, corner2, gradient);
else if ( gradient.span().isDiagonal() )
return _createDiagonalGradientPaint(corner1, corner2, gradient);
else
return _createVerticalOrHorizontalGradientPaint(corner1, corner2, gradient);
}
private static Paint _createConicGradientPaint(
Point2D.Float corner1,
Point2D.Float corner2,
GradientConf gradient
) {
final Color[] colors = gradient.colors();
final float[] fractions = _fractionsFrom(gradient);
float rotation = gradient.rotation() + _rotationBetween(corner1, corner2);
// we normalize the rotation to be between -180 and 180
rotation = ((((rotation+180f) % 360f + 360f) % 360f)-180f);
// Now we convert the fractions to rotations:
for ( int i = 0; i < fractions.length; i++ )
fractions[i] = (fractions[i] * 360f);// (((((fractions[i] * 360f)+180f) % 360f + 360f) % 360f)-180f);
return new ConicalGradientPaint(
true,
corner1,
rotation,
fractions,
colors
);
}
private static void _renderNoise(
final NoiseConf noise,
final LayerRenderConf conf,
final Graphics2D g2d
) {
Paint noisePaint = _createNoisePaint(conf.boxModel(), noise);
Area areaToFill = conf.areas().get(noise.area());
g2d.setPaint(noisePaint);
g2d.fill(areaToFill);
}
static Paint _createNoisePaint(
final BoxModelConf boxModel,
final NoiseConf noise
) {
if ( noise.colors().length == 1 ) {
return noise.colors()[0];
} else {
Size dimensions = boxModel.size();
Outline insets = Outline.none();
switch ( noise.boundary() ) {
case OUTER_TO_EXTERIOR:
insets = Outline.none(); break;
case EXTERIOR_TO_BORDER:
insets = boxModel.margin(); break;
case BORDER_TO_INTERIOR:
insets = boxModel.margin().plus(boxModel.widths()); break;
case INTERIOR_TO_CONTENT:
insets = boxModel.margin().plus(boxModel.widths()).plus(boxModel.padding()); break;
case CENTER_TO_CONTENT:
float verticalInset = dimensions.height().orElse(0f) / 2f;
float horizontalInset = dimensions.width().orElse(0f) / 2f;
insets = Outline.of(verticalInset, horizontalInset);
}
Point2D.Float corner1 = new Point2D.Float(
insets.left().orElse(0f) + noise.offset().x(),
insets.top().orElse(0f) + noise.offset().y()
);
return _createNoisePaint(corner1, noise);
}
}
private static Paint _createNoisePaint(
final Point2D.Float center,
final NoiseConf noise
) {
final Color[] colors = noise.colors();
final float[] fractions = _fractionsFrom(colors, noise.fractions());
float rotation = noise.rotation();
Scale scale = noise.scale();
float scaleX = scale.x();
float scaleY = scale.y();
return new NoiseGradientPaint(
center,
scaleX,
scaleY,
rotation,
fractions,
colors,
noise.function()
);
}
/**
* Renders a shade from the top left corner to the bottom right corner.
*
* @param corner1 The first corner of the shade.
* @param corner2 The second corner of the shade.
* @param gradient The shade to render.
*/
private static Paint _createDiagonalGradientPaint(
Point2D.Float corner1,
Point2D.Float corner2,
final GradientConf gradient
) {
{
final float cx = ( corner1.x + corner2.x ) / 2;
final float cy = ( corner1.y + corner2.y ) / 2;
final float nx = ( corner2.x - corner1.x );
final float ny = ( corner1.y - corner2.y );
/*
The above variables form 2 lines:
1. The line with direction n going through corner1.
2. The line with direction n going through corner2.
*/
// project the center (cx,cy) onto the lines:
corner1 = projectPointOntoLine(corner1, new Point2D.Float(nx, ny), new Point2D.Float(cx, cy));
corner2 = projectPointOntoLine(corner2, new Point2D.Float(nx, ny), new Point2D.Float(cx, cy));
}
final UI.Cycle cycle = gradient.cycle();
final Color[] colors = gradient.colors();
final float size = gradient.size();
final float corner1X = corner1.x;
final float corner1Y = corner1.y;
float corner2X = corner2.x;
float corner2Y = corner2.y;
float[] fractions = _fractionsFrom(gradient);
if ( size >= 0 ) {
float vectorX = corner2X - corner1X;
float vectorY = corner2Y - corner1Y;
float vectorLength2 = (float) Math.sqrt(vectorX * vectorX + vectorY * vectorY);
vectorX = (vectorX / vectorLength2);
vectorY = (vectorY / vectorLength2);
corner2X = corner1X + vectorX * size;
corner2Y = corner1Y + vectorY * size;
}
if ( gradient.rotation() % 360f != 0 ) {
Point2D.Float p1 = new Point2D.Float(corner1X, corner1Y);
Point2D.Float p2 = new Point2D.Float(corner2X, corner2Y);
p2 = _rotatePoint(p1, p2, gradient.rotation());
corner2X = p2.x;
corner2Y = p2.y;
}
if ( colors.length == 2 && gradient.fractions().length == 0 && cycle == UI.Cycle.NONE )
return new GradientPaint(
corner1X, corner1Y, colors[0],
corner2X, corner2Y, colors[1]
);
else
return new LinearGradientPaint(
corner1X, corner1Y,
corner2X, corner2Y,
fractions, colors,
_cycleMethodFrom(cycle)
);
}
private static Paint _createVerticalOrHorizontalGradientPaint(
Point2D.Float corner1,
Point2D.Float corner2,
GradientConf gradient
) {
final UI.Cycle cycle = gradient.cycle();
final Color[] colors = gradient.colors();
final float size = gradient.size();
final float corner1X = corner1.x;
final float corner1Y = corner1.y;
float corner2X = corner2.x;
float corner2Y = corner2.y;
if ( gradient.type() == UI.GradientType.LINEAR ) {
if ( size >= 0 ) {
float vectorX = corner2X - corner1X;
float vectorY = corner2Y - corner1Y;
float vectorLength = (float) Math.sqrt(vectorX * vectorX + vectorY * vectorY);
vectorX = (vectorX / vectorLength);
vectorY = (vectorY / vectorLength);
corner2X = corner1X + vectorX * size;
corner2Y = corner1Y + vectorY * size;
}
}
if (
colors.length == 2 &&
gradient.fractions().length == 0 &&
cycle == UI.Cycle.NONE
) {
return new GradientPaint(
corner1X, corner1Y, colors[0],
corner2X, corner2Y, colors[1]
);
} else {
float[] fractions = _fractionsFrom(gradient);
if ( gradient.rotation() % 360f != 0 ) {
Point2D.Float p1 = new Point2D.Float(corner1X, corner1Y);
Point2D.Float p2 = new Point2D.Float(corner2X, corner2Y);
p2 = _rotatePoint(p1, p2, gradient.rotation());
corner2X = p2.x;
corner2Y = p2.y;
}
return new LinearGradientPaint(
corner1X, corner1Y,
corner2X, corner2Y,
fractions, colors,
_cycleMethodFrom(cycle)
);
}
}
private static Point2D.Float projectPointOntoLine(Point2D.Float A, Point2D.Float n, Point2D.Float C) {
Point2D.Float B = new Point2D.Float(A.x + n.x, A.y + n.y);
float t = ((C.x - A.x) * (B.x - A.x) + (C.y - A.y) * (B.y - A.y)) / ((B.x - A.x) * (B.x - A.x) + (B.y - A.y) * (B.y - A.y));
return new Point2D.Float(A.x + t * (B.x - A.x), A.y + t * (B.y - A.y));
}
private static Paint _createRadialGradientPaint(
Point2D.Float corner1,
Point2D.Float corner2,
GradientConf gradient
) {
final UI.Cycle cycle = gradient.cycle();
final Color[] colors = gradient.colors();
final float size = gradient.size();
final float corner1X = corner1.x;
final float corner1Y = corner1.y;
float corner2X = corner2.x;
float corner2Y = corner2.y;
float[] fractions = _fractionsFrom(gradient);
float radius;
if ( size < 0 )
radius = (float) Math.sqrt(
(corner2X - corner1X) * (corner2X - corner1X) +
(corner2Y - corner1Y) * (corner2Y - corner1Y)
);
else
radius = size;
if ( gradient.focus().equals(Offset.none()) ) {
if ( colors.length == 2 )
return new RadialGradientPaint(
new Point2D.Float(corner1X, corner1Y),
radius,
fractions,
colors,
_cycleMethodFrom(cycle)
);
else
return new RadialGradientPaint(
new Point2D.Float(corner1X, corner1Y),
radius,
fractions,
colors,
_cycleMethodFrom(cycle)
);
} else {
float focusX = corner1X + gradient.focus().x();
float focusY = corner1Y + gradient.focus().y();
if ( gradient.rotation() % 360f != 0 ) {
Point2D.Float p1 = new Point2D.Float(corner1X, corner1Y);
Point2D.Float p2 = new Point2D.Float(focusX, focusY);
p2 = _rotatePoint(p1, p2, gradient.rotation());
focusX = p2.x;
focusY = p2.y;
}
return new RadialGradientPaint(
new Point2D.Float(corner1X, corner1Y),
radius,
new Point2D.Float(focusX, focusY),
fractions,
colors,
_cycleMethodFrom(cycle)
);
}
}
private static MultipleGradientPaint.CycleMethod _cycleMethodFrom(UI.Cycle cycle) {
switch (cycle) {
case NONE: return MultipleGradientPaint.CycleMethod.NO_CYCLE;
case REPEAT: return MultipleGradientPaint.CycleMethod.REPEAT;
case REFLECT: return MultipleGradientPaint.CycleMethod.REFLECT;
default:
log.warn("Unknown cycle method: " + cycle, new Throwable());
return MultipleGradientPaint.CycleMethod.NO_CYCLE;
}
}
private static float[] _fractionsFrom(GradientConf style ) {
Color[] colors = style.colors();
float[] fractions = style.fractions();
return _fractionsFrom(colors, fractions);
}
private static float[] _fractionsFrom(
Color[] colors,
float[] fractions
) {
if ( fractions.length == colors.length )
return fractions;
else if ( fractions.length > colors.length ) {
float[] newFractions = new float[colors.length];
System.arraycopy(fractions, 0, newFractions, 0, colors.length);
return newFractions;
} else {
if ( fractions.length == 0 ) {
fractions = new float[colors.length];
for ( int i = 0; i < colors.length; i++ )
fractions[i] = (float) i / (float) (colors.length - 1);
return fractions;
} else {
float[] newFractions = new float[colors.length];
System.arraycopy(fractions, 0, newFractions, 0, fractions.length);
/*
Now simply complete th missing fractions by linear interpolation
between the last fraction and 1f
*/
float lastFraction = fractions[fractions.length - 1];
float step = (1f - lastFraction) / (colors.length - fractions.length);
for ( int i = fractions.length; i < colors.length; i++ )
newFractions[i] = lastFraction + step * (i - fractions.length + 1);
return newFractions;
}
}
}
/**
* Takes two points {@code p1} and {@code p2} as well as
* a {@code rotation} float representing degrees and returns
* the point {@code p2} rotated around {@code p1} by {@code rotation} degrees.
*/
private static Point2D.Float _rotatePoint(
final Point2D.Float p1,
final Point2D.Float p2,
final float rotation
) {
if ( rotation == 0f )
return p2;
else if ( rotation % 360f == 0f )
return p2;
final double angle = Math.toRadians(rotation);
final double sin = Math.sin(angle);
final double cos = Math.cos(angle);
final double x = p2.x - p1.x;
final double y = p2.y - p1.y;
final double newX = x * cos - y * sin;
final double newY = x * sin + y * cos;
return new Point2D.Float((float) (p1.x + newX), (float) (p1.y + newY));
}
/**
* Takes 2 points and calculates the rotation
* of point 2 around point 1.
*
* @param p1 The first point which serves as the center of rotation.
* @param p2 The second point which is rotated around point 1.
* @return The rotation in degrees.
*/
private static final float _rotationBetween(
final Point2D.Float p1,
final Point2D.Float p2
){
final double x = p2.x - p1.x;
final double y = p2.y - p1.y;
return (float) Math.toDegrees(Math.atan2(y, x));
}
private static void _renderImage(
LayerRenderConf conf,
ImageConf style,
Size componentSize,
Graphics2D g2d
) {
if ( style.primer().isPresent() ) {
g2d.setColor(style.primer().get());
g2d.fill(conf.areas().get(style.clipArea()));
}
style.image().ifPresent( imageIcon -> {
final UI.FitComponent fit = style.fitMode();
final UI.Placement placement = style.placement();
final Outline padding = style.padding();
final int componentWidth = componentSize.width().orElse(0f).intValue();
final int componentHeight = componentSize.height().orElse(0f).intValue();
int imgWidth = style.width().orElse(imageIcon.getIconWidth());
int imgHeight = style.height().orElse(imageIcon.getIconHeight());
if ( fit != UI.FitComponent.NO ) {
if ( imageIcon instanceof SvgIcon) {
// The SvgIcon does the fitting...
imgWidth = style.width().orElse(componentWidth);
imgHeight = style.height().orElse(componentHeight);
} else {
if ( fit == UI.FitComponent.WIDTH_AND_HEIGHT ) {
imgWidth = style.width().orElse(componentWidth);
imgHeight = style.height().orElse(componentHeight);
}
if (
fit == UI.FitComponent.WIDTH ||
(fit == UI.FitComponent.MAX_DIM && componentWidth > componentHeight) ||
(fit == UI.FitComponent.MIN_DIM && componentWidth < componentHeight )
) {
imgWidth = style.width().orElse(componentWidth);
double aspectRatio = (double) imageIcon.getIconHeight() / (double) imageIcon.getIconWidth();
// We preserve the aspect ratio:
imgHeight = (int) (imgWidth * aspectRatio);
} if (
fit == UI.FitComponent.HEIGHT ||
(fit == UI.FitComponent.MAX_DIM && componentWidth < componentHeight) ||
(fit == UI.FitComponent.MIN_DIM && componentWidth > componentHeight )
) {
imgHeight = style.height().orElse(componentHeight);
double aspectRatio = (double) imageIcon.getIconWidth() / (double) imageIcon.getIconHeight();
// We preserve the aspect ratio:
imgWidth = (int) (imgHeight * aspectRatio);
}
}
imgWidth = imgWidth >= 0 ? imgWidth : componentWidth;
imgHeight = imgHeight >= 0 ? imgHeight : componentHeight;
}
if ( imageIcon instanceof SvgIcon ) {
SvgIcon svgIcon = (SvgIcon) imageIcon;
FloatSize size = svgIcon.getSvgSize();
imgWidth = imgWidth >= 0 ? imgWidth : (int) size.width;
imgHeight = imgHeight >= 0 ? imgHeight : (int) size.height;
}
int x = style.horizontalOffset();
int y = style.verticalOffset();
switch ( placement ) {
case TOP:
x += (componentWidth - imgWidth) / 2;
break;
case LEFT:
y += (componentHeight - imgHeight) / 2;
break;
case BOTTOM:
x += (componentWidth - imgWidth) / 2;
y += componentHeight - imgHeight;
break;
case RIGHT:
x += componentWidth - imgWidth;
y += (componentHeight - imgHeight) / 2;
break;
case TOP_LEFT: break;
case TOP_RIGHT:
x += componentWidth - imgWidth;
break;
case BOTTOM_LEFT:
y += componentHeight - imgHeight;
break;
case BOTTOM_RIGHT:
x += componentWidth - imgWidth;
y += componentHeight - imgHeight;
break;
case CENTER:
case UNDEFINED:
x += (componentWidth - imgWidth) / 2;
y += (componentHeight - imgHeight) / 2;
break;
default:
throw new IllegalArgumentException("Unknown placement: " + placement);
}
// We apply the padding:
x += padding.left().orElse(0f).intValue();
y += padding.top().orElse(0f).intValue();
imgWidth -= padding.left().orElse(0f).intValue() + padding.right().orElse(0f).intValue();
imgHeight -= padding.top().orElse(0f).intValue() + padding.bottom().orElse(0f).intValue();
if ( imageIcon instanceof SvgIcon ) {
SvgIcon svgIcon = (SvgIcon) imageIcon;
if ( imgWidth > -1 && svgIcon.getIconWidth() < 0 )
svgIcon = svgIcon.withIconWidth(imgWidth);
if ( imgHeight > -1 && svgIcon.getIconHeight() < 0 )
svgIcon = svgIcon.withIconHeight(imgHeight);
imageIcon = svgIcon;
}
final boolean repeat = style.repeat();
final float opacity = style.opacity();
final Shape oldClip = g2d.getClip();
Shape newClip = conf.areas().get(style.clipArea());
// We merge the new clip with the old one:
if ( newClip != null && oldClip != null )
newClip = StyleUtil.intersect( newClip, oldClip );
g2d.setClip(newClip);
if ( !repeat && imageIcon instanceof SvgIcon ) {
SvgIcon svgIcon = ((SvgIcon) imageIcon).withFitComponent(fit);
svgIcon.withPreferredPlacement(UI.Placement.CENTER)
.paintIcon(null, g2d, x, y, imgWidth, imgHeight);
}
else
{
Image image;
if ( imageIcon instanceof SvgIcon) {
SvgIcon svgIcon = (SvgIcon) imageIcon;
svgIcon = svgIcon.withIconWidth(imgWidth);
svgIcon = svgIcon.withIconHeight(imgHeight);
image = svgIcon.getImage(); // This will render the SVGIcon with the new size
}
else
image = imageIcon.getImage();
Composite oldComposite = g2d.getComposite();
try {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
if (repeat) {
Paint oldPaint = g2d.getPaint();
try {
g2d.setPaint(new TexturePaint((BufferedImage) image, new Rectangle(x, y, imgWidth, imgHeight)));
g2d.fill(conf.areas().get(UI.ComponentArea.BODY));
} finally {
g2d.setPaint(oldPaint);
}
}
else
g2d.drawImage(image, x, y, imgWidth, imgHeight, null);
} finally {
g2d.setComposite(oldComposite);
}
}
g2d.setClip(oldClip);
});
}
private static void _renderText(
final TextConf text,
final LayerRenderConf conf,
final Graphics2D g2d
) {
if ( text.content().isEmpty() )
return;
final BoxModelConf boxModel = conf.boxModel();
final Font initialFont = g2d.getFont();
final Shape oldClip = g2d.getClip();
final String textToRender = text.content();
final UI.ComponentArea clipArea = text.clipArea();
final UI.ComponentBoundary placementBoundary = text.placementBoundary();
final UI.Placement placement = text.placement();
final Offset offset = text.offset();
Font font = Optional.ofNullable(initialFont).orElse(new Font(Font.DIALOG, Font.PLAIN, UI.scale(12)));
font = text.fontConf().createDerivedFrom(font, boxModel).orElse(font);
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics(font);
Rectangle2D rect = fm.getStringBounds(textToRender, g2d);
Outline insets = _insetsFrom(placementBoundary, boxModel);
float x = insets.left().orElse(0f); // Top left is always the starting point
float y = insets.top().orElse(0f) ;
{
float leftX = insets.left().orElse(0f);
float topY = insets.top().orElse(0f);
float localWidth = boxModel.size().width().orElse(0f) - (leftX + insets.right().orElse(0f));
float localHeight = boxModel.size().height().orElse(0f) - (topY + insets.bottom().orElse(0f));
float rightX = leftX + localWidth;
float bottomY = topY + localHeight;
switch (placement) {
case CENTER:
float centerX = leftX + localWidth / 2f;
float centerY = topY + localHeight / 2f;
x = centerX - (float) rect.getWidth() / 2f;
y = centerY - (float) rect.getHeight() / 2f;
break;
case TOP:
x = leftX + (localWidth - (float) rect.getWidth()) / 2f;
y = topY;
break;
case LEFT:
x = leftX;
y = topY + (localHeight - (float) rect.getHeight()) / 2f;
break;
case BOTTOM:
x = leftX + (localWidth - (float) rect.getWidth()) / 2f;
y = bottomY - (float) rect.getHeight();
break;
case RIGHT:
x = rightX - (float) rect.getWidth();
y = topY + (localHeight - (float) rect.getHeight()) / 2f;
break;
case TOP_LEFT:
x = leftX;
y = topY;
break;
case TOP_RIGHT:
x = rightX - (float) rect.getWidth();
y = topY;
break;
case BOTTOM_LEFT:
x = leftX;
y = bottomY - (float) rect.getHeight();
break;
case BOTTOM_RIGHT:
x = rightX - (float) rect.getWidth();
y = bottomY - (float) rect.getHeight();
break;
case UNDEFINED:
break;
}
}
try {
x += (offset.x() - (float) rect.getX());
y += (offset.y() - (float) rect.getY());
g2d.setClip(conf.areas().get(clipArea));
// Render the text:
g2d.drawString(textToRender, x, y);
} finally {
g2d.setFont(initialFont);
g2d.setClip(oldClip);
}
}
static void renderParentFilter(
FilterConf filterConf,
BufferedImage parentRendering,
Graphics2D g2d,
int offsetX,
int offsetY,
BoxModelConf boxModelConf
) {
final Size size = boxModelConf.size();
final float width = size.width().orElse(0f);
final float height = size.height().orElse(0f);
final Offset center = filterConf.offset();
final Scale scale = filterConf.scale();
final KernelConf kernel = filterConf.kernel();
final float blur = filterConf.blur();
BufferedImage filtered = parentRendering;
if ( !center.equals(Offset.none()) || !scale.equals(Scale.none()) ) {
if ( scale.equals(Scale.none()) ) {
offsetX += (int) center.x();
offsetY += (int) center.y();
} else {
AffineTransform at = new AffineTransform();
float vx = center.x() + offsetX + width / 2f;
float vy = center.y() + offsetY + height / 2f;
at.translate(vx, vy);
at.scale(scale.x(), scale.y());
at.translate(-vx, -vy);
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
filtered = scaleOp.filter(filtered, null);
}
}
if ( !kernel.equals(KernelConf.none()) ) {
Kernel awtKernel = kernel.toAwtKernel();
ConvolveOp convolve = new ConvolveOp(awtKernel, ConvolveOp.EDGE_NO_OP, null);
filtered = convolve.filter(filtered, null);
}
if ( blur > 0 ) {
Kernel blurKernelHorizontal = _makeKernel(blur, false);
ConvolveOp blurOp = new ConvolveOp(blurKernelHorizontal, ConvolveOp.EDGE_NO_OP, null);
BufferedImage blurred = blurOp.filter(filtered, null);
Kernel blurKernelVertical = _makeKernel(blur, true);
blurOp = new ConvolveOp(blurKernelVertical, ConvolveOp.EDGE_NO_OP, null);
filtered = blurOp.filter(blurred, filtered);
}
Shape oldClip = g2d.getClip();
try {
ComponentAreas areas = boxModelConf.areas();
Shape newClip = areas.get(filterConf.area());
if (newClip == null) {
newClip = new Rectangle(0, 0, (int) width, (int) height);
}
g2d.setClip(newClip);
g2d.drawImage(filtered, -offsetX, -offsetY, null);
} catch (Exception e) {
log.error("Failed to successfully render filtered parent buffer!", e);
} finally {
g2d.setClip(oldClip);
}
}
private static Kernel _makeKernel( float radius, boolean transpose ) {
final int maxRadius = (int)Math.ceil(radius);
final int rows = maxRadius * 2 + 1;
final float[] matrix = new float[rows];
final float sigma = radius / 3;
final float sigma22 = 2*sigma*sigma;
final float sigmaPi2 = (float) ( 2 * Math.PI * sigma );
final float sqrtSigmaPi2 = (float)Math.sqrt(sigmaPi2);
final float radius2 = radius*radius;
float total = 0;
int index = 0;
for (int row = -maxRadius; row <= maxRadius; row++) {
float distance = row*row;
if (distance > radius2)
matrix[index] = 0;
else
matrix[index] = (float)Math.exp(-distance/sigma22) / sqrtSigmaPi2;
total += matrix[index];
index++;
}
for ( int i = 0; i < rows; i++ )
matrix[i] /= total;
return new Kernel( transpose ? 1 : rows, transpose ? rows : 1, matrix );
}
}