Skip to content

Commit 0aa9a4a

Browse files
authored
Merge pull request #157 from DemchaAV/feat/v1.8-gradients
feat(style): gradient fills - Series A (DocumentPaint shadings)
2 parents 6ceb32d + 4632726 commit 0aa9a4a

18 files changed

Lines changed: 497 additions & 99 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Entries land here as they merge.
1515
series, type/colour-agnostic), sealed `ChartSpec` (`bar()` / `line()` with
1616
axis, legend, value-label, and sizing knobs), `ChartStyle` (nullable-field
1717
cascade merged over `ChartTheme` tokens, per-series paint overrides), and
18-
`DocumentPaint` (solid today; linear/radial gradient stops reserved for the
19-
gradient work). Charts compile at layout time into existing primitives
18+
`DocumentPaint` (solid, linear, and radial — see the gradient entry below).
19+
Charts compile at layout time into existing primitives
2020
(shapes, lines, paragraphs) via `ChartDefinition` — no new render handlers,
2121
deterministic geometry, covered by the standard snapshot machinery; any
2222
fixed-layout backend renders charts with no chart-specific code, while the
@@ -68,6 +68,20 @@ Entries land here as they merge.
6868
configurable halo chip (`ChartStyle.valueLabelHalo(...)`, themed white) so
6969
digits stay legible where lines cross them, and deterministically flip below
7070
their point when two series' labels would collide at the same category.
71+
- **Gradient fills** (`@since 1.8.0`). `DocumentPaint` graduates to
72+
`com.demcha.compose.document.style` as the shared paint vocabulary, and
73+
gradients now actually render: `ShapeNode` gains an optional `fillPaint`
74+
(`ShapeBuilder.fill(paint)`) that wins over `fillColor`. The PDF backend
75+
paints `DocumentPaint.linear` as a native axial shading (0° = left→right,
76+
90° = bottom→top; two stops exponential, more stops stitched) and
77+
`DocumentPaint.radial` as a radial shading reaching the farthest corner,
78+
clipped to the shape path — rounded corners included. Chart bars now carry
79+
their full series paint, so a gradient palette renders as gradients instead
80+
of degrading to the first stop. Solid paints normalise to the plain
81+
fill-colour path, keeping existing documents byte-identical; backends
82+
without shading support fall back to `primaryColor()` by contract. The
83+
flagship `BusinessReportExample` hero is now fully vector — gradient-sky
84+
shape plus polygon mountain ranges replace the last Graphics2D raster.
7185
- **Translucent shape colours** (`@since 1.8.0`). `DocumentColor.rgba(r, g, b, a)`
7286
and `withOpacity(0..1)`: the PDF backend honours the alpha channel on shape
7387
fills and strokes (rectangles/panels/bars, chart value-label halos, ellipse
-4.34 KB
Binary file not shown.

examples/src/main/java/com/demcha/examples/features/charts/ChartShowcaseExample.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import com.demcha.compose.document.chart.ChartSize;
99
import com.demcha.compose.document.chart.ChartSpec;
1010
import com.demcha.compose.document.chart.ChartStyle;
11-
import com.demcha.compose.document.chart.DocumentPaint;
11+
import com.demcha.compose.document.style.DocumentPaint;
1212
import com.demcha.compose.document.chart.LegendPosition;
1313
import com.demcha.compose.document.chart.NumberFormatSpec;
1414
import com.demcha.compose.document.chart.PointMarker;

examples/src/main/java/com/demcha/examples/flagships/BusinessReportExample.java

Lines changed: 59 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
import com.demcha.compose.document.api.DocumentSession;
66
import com.demcha.compose.document.dsl.ParagraphBuilder;
77
import com.demcha.compose.document.dsl.SectionBuilder;
8-
import com.demcha.compose.document.image.DocumentImageData;
9-
import com.demcha.compose.document.image.DocumentImageFitMode;
108
import com.demcha.compose.document.node.DocumentNode;
119
import com.demcha.compose.document.node.LayerAlign;
1210
import com.demcha.compose.document.node.TextAlign;
@@ -21,13 +19,6 @@
2119
import com.demcha.compose.font.FontName;
2220
import com.demcha.examples.support.ExampleOutputPaths;
2321

24-
import javax.imageio.ImageIO;
25-
import java.awt.GradientPaint;
26-
import java.awt.Graphics2D;
27-
import java.awt.Polygon;
28-
import java.awt.RenderingHints;
29-
import java.awt.image.BufferedImage;
30-
import java.io.ByteArrayOutputStream;
3122
import java.nio.file.Path;
3223

3324
/**
@@ -86,8 +77,6 @@ private BusinessReportExample() {
8677
public static Path generate() throws Exception {
8778
Path outputFile = ExampleOutputPaths.prepare("flagships", "business-report.pdf");
8879

89-
DocumentImageData heroImage = DocumentImageData.fromBytes(renderHeroImage(380, 200));
90-
9180
try (DocumentSession document = GraphCompose.document(outputFile)
9281
.pageSize(DocumentPageSize.A4)
9382
.pageBackground(PAPER)
@@ -155,22 +144,17 @@ public static Path generate() throws Exception {
155144
.margin(new DocumentInsets(4, 0, 0, 0))))
156145
.addSection("HeroImage", section -> section
157146
.padding(DocumentInsets.zero())
158-
// Hero image lives inside a rounded
159-
// shape container so the navy edges
160-
// soften into a frame instead of
161-
// bleeding straight to the page edge.
147+
// The hero scene is fully vector now: a
148+
// gradient-sky shape with two polygon
149+
// mountain ranges, clipped to the rounded
150+
// frame. No raster, no AWT.
162151
.addContainer(frame -> frame
163152
.name("HeroFrame")
164153
.roundedRect(210, 110, 12)
165154
.fillColor(NAVY_DARK)
166155
.stroke(DocumentStroke.of(GOLD, 0.6))
167156
.clipPolicy(ClipPolicy.CLIP_PATH)
168-
.center(new com.demcha.compose.document.dsl.ImageBuilder()
169-
.name("HeroImage")
170-
.source(heroImage)
171-
.size(204, 104)
172-
.fitMode(DocumentImageFitMode.COVER)
173-
.build()))))
157+
.center(buildHeroScene(204, 104)))))
174158

175159
// Three KPI cards
176160
.addRow("KpiRow", row -> row
@@ -444,8 +428,8 @@ private static DocumentNode buildChart() {
444428
.build();
445429
com.demcha.compose.document.chart.ChartStyle style =
446430
com.demcha.compose.document.chart.ChartStyle.builder()
447-
.seriesPaint(0, com.demcha.compose.document.chart.DocumentPaint.solid(NAVY))
448-
.seriesPaint(1, com.demcha.compose.document.chart.DocumentPaint.solid(GOLD))
431+
.seriesPaint(0, com.demcha.compose.document.style.DocumentPaint.solid(NAVY))
432+
.seriesPaint(1, com.demcha.compose.document.style.DocumentPaint.solid(GOLD))
449433
.build();
450434
return new com.demcha.compose.document.node.ChartNode(
451435
"PerformanceChart", spec, style, null, null);
@@ -454,53 +438,60 @@ private static DocumentNode buildChart() {
454438
// ─────────────────── Hero image generator ─────────────────────
455439

456440
/**
457-
* Renders a simple gradient hero image (sunset sky + mountain
458-
* silhouette) so the example does not depend on any external image
459-
* asset. The result is encoded as PNG bytes and embedded directly.
460-
*
461-
* <p>This is the one remaining raster block in the example, and it is
462-
* deliberate: the sky requires a smooth linear gradient, which the engine
463-
* does not paint natively yet ({@code DocumentPaint.linear} is reserved
464-
* for the gradient work). Once gradients land, the mountains become
465-
* {@code PolygonNode}s, the glow a translucent fill, and this method goes
466-
* away like the old chart raster did.</p>
441+
* Builds the hero scene fully from vector primitives: a gradient-sky
442+
* shape (warm cream at the horizon rising into slate) with two polygon
443+
* mountain ranges — the distant one translucent, the foreground one
444+
* solid. The same sunset the old Graphics2D raster painted, now native:
445+
* deterministic, crisp at any zoom, and free of the AWT dependency.
467446
*/
468-
private static byte[] renderHeroImage(int width, int height) throws Exception {
469-
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
470-
Graphics2D g = img.createGraphics();
471-
try {
472-
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
473-
// Sky gradient: slate at the top, warm cream at the horizon.
474-
g.setPaint(new GradientPaint(
475-
0, 0, new java.awt.Color(58, 70, 100),
476-
0, height * 0.7f, new java.awt.Color(218, 196, 162)));
477-
g.fillRect(0, 0, width, height);
478-
// Distant mountain silhouette.
479-
g.setColor(new java.awt.Color(50, 62, 88, 230));
480-
Polygon farRange = new Polygon(
481-
new int[]{0, (int) (width * 0.18), (int) (width * 0.36), (int) (width * 0.54),
482-
(int) (width * 0.72), (int) (width * 0.88), width, width, 0},
483-
new int[]{(int) (height * 0.55), (int) (height * 0.40), (int) (height * 0.50),
484-
(int) (height * 0.34), (int) (height * 0.46), (int) (height * 0.36),
485-
(int) (height * 0.50), height, height},
486-
9);
487-
g.fill(farRange);
488-
// Foreground mountain wedge.
489-
g.setColor(new java.awt.Color(28, 36, 60));
490-
Polygon foreRange = new Polygon(
491-
new int[]{0, (int) (width * 0.22), (int) (width * 0.40), (int) (width * 0.60),
492-
(int) (width * 0.82), width, width, 0},
493-
new int[]{(int) (height * 0.78), (int) (height * 0.55), (int) (height * 0.68),
494-
(int) (height * 0.50), (int) (height * 0.62), (int) (height * 0.74),
495-
height, height},
496-
8);
497-
g.fill(foreRange);
498-
} finally {
499-
g.dispose();
447+
private static DocumentNode buildHeroScene(double width, double height) {
448+
com.demcha.compose.document.node.ShapeNode sky =
449+
new com.demcha.compose.document.dsl.ShapeBuilder()
450+
.name("HeroSky")
451+
.size(width, height)
452+
.fill(new com.demcha.compose.document.style.DocumentPaint.Linear(
453+
java.util.List.of(
454+
new com.demcha.compose.document.style.DocumentPaint.Stop(
455+
0.0, DocumentColor.rgb(218, 196, 162)),
456+
new com.demcha.compose.document.style.DocumentPaint.Stop(
457+
0.30, DocumentColor.rgb(218, 196, 162)),
458+
new com.demcha.compose.document.style.DocumentPaint.Stop(
459+
1.0, DocumentColor.rgb(58, 70, 100))),
460+
90.0))
461+
.build();
462+
com.demcha.compose.document.node.PolygonNode farRange =
463+
new com.demcha.compose.document.node.PolygonNode(
464+
"HeroFarRange", width, height,
465+
heroPoints(new double[][] {
466+
{0, .45}, {.18, .60}, {.36, .50}, {.54, .66},
467+
{.72, .54}, {.88, .64}, {1, .50}, {1, 0}, {0, 0}}),
468+
DocumentColor.rgba(50, 62, 88, 230), null,
469+
DocumentInsets.zero(), DocumentInsets.zero());
470+
com.demcha.compose.document.node.PolygonNode foreRange =
471+
new com.demcha.compose.document.node.PolygonNode(
472+
"HeroForeRange", width, height,
473+
heroPoints(new double[][] {
474+
{0, .22}, {.22, .45}, {.40, .32}, {.60, .50},
475+
{.82, .38}, {1, .26}, {1, 0}, {0, 0}}),
476+
DocumentColor.rgb(28, 36, 60), null,
477+
DocumentInsets.zero(), DocumentInsets.zero());
478+
return new com.demcha.compose.document.node.CanvasLayerNode(
479+
"HeroScene", width, height,
480+
java.util.List.of(
481+
new com.demcha.compose.document.node.CanvasChild(sky, 0, 0),
482+
new com.demcha.compose.document.node.CanvasChild(farRange, 0, 0),
483+
new com.demcha.compose.document.node.CanvasChild(foreRange, 0, 0)),
484+
ClipPolicy.OVERFLOW_VISIBLE, DocumentInsets.zero(), DocumentInsets.zero());
485+
}
486+
487+
private static java.util.List<com.demcha.compose.document.style.ShapePoint> heroPoints(
488+
double[][] xy) {
489+
java.util.List<com.demcha.compose.document.style.ShapePoint> points =
490+
new java.util.ArrayList<>(xy.length);
491+
for (double[] p : xy) {
492+
points.add(new com.demcha.compose.document.style.ShapePoint(p[0], p[1]));
500493
}
501-
ByteArrayOutputStream out = new ByteArrayOutputStream();
502-
ImageIO.write(img, "png", out);
503-
return out.toByteArray();
494+
return points;
504495
}
505496

506497
// ─────────────────── Text styles ──────────────────────────────

0 commit comments

Comments
 (0)