PlotRenderTheme.java
package org.hammer.audio.ui.theme;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
/** Shared plot colors, fonts and drawing helpers for Swing renderers. */
public final class PlotRenderTheme {
public static final Color PANEL_BACKGROUND = new Color(24, 27, 32);
public static final Color PLOT_BACKGROUND = new Color(18, 21, 27);
public static final Color GRID_MAJOR = new Color(88, 102, 125, 70);
public static final Color GRID_MINOR = new Color(88, 102, 125, 35);
public static final Color AXIS_COLOR = new Color(118, 134, 160, 120);
public static final Color TEXT_PRIMARY = new Color(222, 226, 234);
public static final Color TEXT_MUTED = new Color(159, 171, 191);
public static final Color CENTER_LINE = new Color(129, 110, 189, 120);
public static final Color WAVEFORM_LEFT = new Color(104, 188, 255, 210);
public static final Color WAVEFORM_RIGHT = new Color(102, 235, 187, 180);
public static final Color SPECTRUM_FILL = new Color(78, 152, 255, 95);
public static final Color SPECTRUM_LINE = new Color(120, 188, 255, 220);
public static final Color HIGHLIGHT = new Color(255, 164, 91, 210);
public static final Color PHASE_TRACE = new Color(132, 198, 255, 145);
public static final Font LABEL_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
public static final Font TITLE_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 12);
public static final BasicStroke GRID_STROKE = new BasicStroke(1f);
public static final BasicStroke AXIS_STROKE = new BasicStroke(1.2f);
public static final BasicStroke TRACE_STROKE =
new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
public static final BasicStroke THIN_TRACE_STROKE =
new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
public static final BasicStroke PEAK_STROKE =
new BasicStroke(
1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10f, new float[] {4f, 4f}, 0f);
private static final double DB_FLOOR = -80.0d;
private static final double MAG_EPSILON = 1.0e-7d;
private static final int TICK_SIZE = 4;
private PlotRenderTheme() {}
/** Enables high quality 2D rendering hints. */
public static void applyQualityRendering(Graphics2D g2) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2.setRenderingHint(
RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2.setRenderingHint(
RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
}
/** Draws panel and plot backgrounds. */
public static void drawPlotBackground(
Graphics2D g2, int width, int height, Rectangle plotBounds) {
g2.setColor(PANEL_BACKGROUND);
g2.fillRect(0, 0, width, height);
g2.setColor(PLOT_BACKGROUND);
g2.fillRect(plotBounds.x, plotBounds.y, plotBounds.width, plotBounds.height);
}
/** Draws evenly spaced major/minor grid lines and a border around plot bounds. */
public static void drawGrid(
Graphics2D g2, Rectangle plotBounds, int verticalSteps, int horizontalSteps) {
if (plotBounds.width <= 1 || plotBounds.height <= 1) {
return;
}
int safeV = Math.max(1, verticalSteps);
int safeH = Math.max(1, horizontalSteps);
g2.setStroke(GRID_STROKE);
for (int i = 1; i < safeV; i++) {
int x = plotBounds.x + (int) Math.round(i * (plotBounds.width - 1.0) / safeV);
g2.setColor(i % 2 == 0 ? GRID_MAJOR : GRID_MINOR);
g2.drawLine(x, plotBounds.y, x, plotBounds.y + plotBounds.height - 1);
}
for (int i = 1; i < safeH; i++) {
int y = plotBounds.y + (int) Math.round(i * (plotBounds.height - 1.0) / safeH);
g2.setColor(i % 2 == 0 ? GRID_MAJOR : GRID_MINOR);
g2.drawLine(plotBounds.x, y, plotBounds.x + plotBounds.width - 1, y);
}
g2.setColor(AXIS_COLOR);
g2.setStroke(AXIS_STROKE);
g2.drawRect(plotBounds.x, plotBounds.y, plotBounds.width - 1, plotBounds.height - 1);
}
/** Draws an empty-state message centered in plot bounds. */
public static void drawEmptyState(Graphics2D g2, Rectangle plotBounds, String message) {
g2.setFont(LABEL_FONT);
g2.setColor(TEXT_MUTED);
int textWidth = g2.getFontMetrics().stringWidth(message);
int x = plotBounds.x + Math.max(6, (plotBounds.width - textWidth) / 2);
int y = plotBounds.y + plotBounds.height / 2;
g2.drawString(message, x, y);
}
/** Draws top-left title text. */
public static void drawTitle(Graphics2D g2, int x, int y, String title) {
g2.setFont(TITLE_FONT);
g2.setColor(TEXT_PRIMARY);
g2.drawString(title, x, y);
}
/** Draws label text. */
public static void drawLabel(Graphics2D g2, int x, int y, String label) {
g2.setFont(LABEL_FONT);
g2.setColor(TEXT_MUTED);
g2.drawString(label, x, y);
}
/** Draws a centered X-axis label in the bottom plot margin. */
public static void drawXAxisLabel(Graphics2D g2, Rectangle plotBounds, String label) {
g2.setFont(LABEL_FONT);
g2.setColor(TEXT_MUTED);
int textWidth = g2.getFontMetrics().stringWidth(label);
int x = plotBounds.x + Math.max(0, (plotBounds.width - textWidth) / 2);
int y = plotBounds.y + plotBounds.height + 16;
g2.drawString(label, x, y);
}
/** Draws a rotated Y-axis label in the left plot margin. */
public static void drawYAxisLabel(Graphics2D g2, Rectangle plotBounds, String label) {
g2.setFont(LABEL_FONT);
g2.setColor(TEXT_MUTED);
int textWidth = g2.getFontMetrics().stringWidth(label);
int x = Math.max(10, plotBounds.x - 34);
int y = plotBounds.y + plotBounds.height / 2 + textWidth / 2;
AffineTransform savedTransform = g2.getTransform();
try {
g2.rotate(-Math.PI / 2.0, x, y);
g2.drawString(label, x, y);
} finally {
g2.setTransform(savedTransform);
}
}
/**
* Draws bottom X-axis ticks at normalized positions in {@code [0, 1]}.
*
* @param g2 graphics context to draw into
* @param plotBounds plot rectangle whose bottom edge receives the ticks
* @param positions normalized left-to-right tick positions
* @param labels tick labels, one per position
*/
public static void drawXTicks(
Graphics2D g2, Rectangle plotBounds, double[] positions, String[] labels) {
validateTicks(positions, labels);
g2.setFont(LABEL_FONT);
g2.setStroke(AXIS_STROKE);
int baseline = plotBounds.y + plotBounds.height - 1;
for (int i = 0; i < positions.length; i++) {
int x = plotBounds.x + (int) Math.round(clamp01(positions[i]) * (plotBounds.width - 1));
g2.setColor(AXIS_COLOR);
g2.drawLine(x, baseline, x, baseline + TICK_SIZE);
g2.setColor(TEXT_MUTED);
int textWidth = g2.getFontMetrics().stringWidth(labels[i]);
int labelX = clamp(x - textWidth / 2, 0, plotBounds.x + plotBounds.width - textWidth);
g2.drawString(labels[i], labelX, baseline + 16);
}
}
/**
* Draws left Y-axis ticks at normalized top-to-bottom positions in {@code [0, 1]}.
*
* @param g2 graphics context to draw into
* @param plotBounds plot rectangle whose left edge receives the ticks
* @param positions normalized top-to-bottom tick positions
* @param labels tick labels, one per position
*/
public static void drawYTicks(
Graphics2D g2, Rectangle plotBounds, double[] positions, String[] labels) {
validateTicks(positions, labels);
g2.setFont(LABEL_FONT);
g2.setStroke(AXIS_STROKE);
for (int i = 0; i < positions.length; i++) {
int y = plotBounds.y + (int) Math.round(clamp01(positions[i]) * (plotBounds.height - 1));
g2.setColor(AXIS_COLOR);
g2.drawLine(plotBounds.x - TICK_SIZE, y, plotBounds.x, y);
g2.setColor(TEXT_MUTED);
int textWidth = g2.getFontMetrics().stringWidth(labels[i]);
int labelX = Math.max(0, plotBounds.x - TICK_SIZE - 4 - textWidth);
g2.drawString(labels[i], labelX, y + 4);
}
}
private static void validateTicks(double[] positions, String[] labels) {
if (positions.length != labels.length) {
throw new IllegalArgumentException("positions and labels must have the same length");
}
}
private static double clamp01(double value) {
return Math.max(0.0d, Math.min(1.0d, value));
}
private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
/** Converts linear magnitude to clipped dB FS display value. */
public static double magnitudeToDb(float magnitude) {
double db = 20.0d * Math.log10(Math.max(MAG_EPSILON, magnitude));
return Math.max(DB_FLOOR, db);
}
/** Maps dB value into a [0, 1] normalized range based on configured dB floor. */
public static double normalizedDb(double db) {
return Math.max(0d, Math.min(1d, (db - DB_FLOOR) / Math.abs(DB_FLOOR)));
}
/** Converts magnitude directly to normalized dB display range [0, 1]. */
public static double normalizedMagnitude(float magnitude) {
return normalizedDb(magnitudeToDb(magnitude));
}
}