Add EffectPainter2D and WavePainter2D versions

- also fix EffectPainterTest
This commit is contained in:
Anton Tarasov
2018-03-14 18:35:36 +03:00
parent e0b966650e
commit 6f804df909
4 changed files with 427 additions and 277 deletions
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
* Copyright 2000-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,18 +15,9 @@
*/
package com.intellij.ui.paint;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.JBHiDPIScaledImage;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.RegionPainter;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.WavePainter;
import java.awt.*;
import java.awt.font.LineMetrics;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Sergey.Malenkov
@@ -48,12 +39,7 @@ public enum EffectPainter implements RegionPainter<Font> {
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
g.drawLine(x, y + 1, x + width, y + 1);
}
else {
paintUnderline(g, x, y, width, height, font, 1, this);
}
EffectPainter2D.LINE_UNDERSCORE.paint(g, x, y, width, height, font);
}
},
/**
@@ -72,13 +58,7 @@ public enum EffectPainter implements RegionPainter<Font> {
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
int h = JBUI.scale(Registry.intValue("editor.bold.underline.height", 2));
g.fillRect(x, y, width, h);
}
else {
paintUnderline(g, x, y, width, height, font, 2, this);
}
EffectPainter2D.BOLD_LINE_UNDERSCORE.paint(g, x, y, width, height, font);
}
},
/**
@@ -97,7 +77,7 @@ public enum EffectPainter implements RegionPainter<Font> {
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
paintUnderline(g, x, y, width, height, font, 2, this);
EffectPainter2D.BOLD_DOTTED_UNDERSCORE.paint(g, x, y, width, height, font);
}
},
/**
@@ -116,15 +96,7 @@ public enum EffectPainter implements RegionPainter<Font> {
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
WavePainter.forColor(g.getColor()).paint(g, x, x + width, y + height);
}
else if (Registry.is("ide.text.effect.new.metrics")) {
paintUnderline(g, x, y, width, height, font, 3, this);
}
else if (width > 0 && height > 0) {
Cached.WAVE_UNDERSCORE.paint(g, x, y, width, height, null);
}
EffectPainter2D.WAVE_UNDERSCORE.paint(g, x, y, width, height, font);
}
},
/**
@@ -143,191 +115,7 @@ public enum EffectPainter implements RegionPainter<Font> {
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
if (width > 0 && height > 0) {
if (!Registry.is("ide.text.effect.new.metrics")) {
drawLineCentered(g, x, y - height, width, height, 1, this);
}
else {
if (font == null) font = g.getFont();
LineMetrics metrics = font.getLineMetrics("", g.getFontRenderContext());
int offset = (int)(0.5 - metrics.getStrikethroughOffset());
int thickness = Math.max(1, (int)(0.5 + metrics.getStrikethroughThickness()));
drawLine(g, x, y - offset, width, thickness, this);
}
}
}
};
private static int getMaxHeight(int height) {
return height > 7 && Registry.is("ide.text.effect.new.scale") ? height >> 1 : 3;
}
private static void paintUnderline(Graphics2D g, int x, int y, int width, int height, Font font, int thickness, EffectPainter painter) {
if (width > 0 && height > 0) {
if (Registry.is("ide.text.effect.new.metrics")) {
if (font == null) font = g.getFont();
LineMetrics metrics = font.getLineMetrics("", g.getFontRenderContext());
thickness = Math.max(thickness, (int)(0.5 + thickness * metrics.getUnderlineThickness()));
int offset = Math.min(height - thickness, Math.max(1, (int)(0.5 + metrics.getUnderlineOffset())));
if (offset < 1) {
offset = height > 3 ? 1 : 0;
thickness = height - offset;
}
drawLine(g, x, y + offset, width, thickness, painter);
}
else {
if (height > 3) {
int max = getMaxHeight(height);
y += height - max;
height = max;
if (thickness > 1 && height > 3) {
thickness = JBUI.scale(thickness);
}
}
drawLineCentered(g, x, y, width, height, thickness, painter);
}
}
}
private static void drawLineCentered(Graphics2D g, int x, int y, int width, int height, int thickness, EffectPainter painter) {
int offset = height - thickness;
if (offset > 0) {
y += offset - (offset >> 1);
height = thickness;
}
drawLine(g, x, y, width, height, painter);
}
private static void drawLine(Graphics2D g, int x, int y, int width, int height, EffectPainter painter) {
if (painter == BOLD_DOTTED_UNDERSCORE) {
int dx = (x % height + height) % height;
int w = width + dx;
int dw = (w % height + height) % height;
Cached.BOLD_DOTTED_UNDERSCORE.paint(g, x - dx, y, dw == 0 ? w : w - dw + height, height, null);
}
else if (painter == WAVE_UNDERSCORE) {
Cached.WAVE_UNDERSCORE.paint(g, x, y, width, height, null);
}
else {
g.fillRect(x, y, width, height);
}
}
private enum Cached implements RegionPainter<Paint> {
BOLD_DOTTED_UNDERSCORE {
@Override
int getPeriod(int height) {
return height;
}
@Override
void paintImage(Graphics2D g, int width, int height, int period) {
Integer round = period <= 2 && !UIUtil.isJreHiDPI(g) ? null : period;
for (int dx = 0; dx < width; dx += period + period) {
RectanglePainter.FILL.paint(g, dx, 0, period, period, round);
}
}
},
WAVE_UNDERSCORE {
private final BasicStroke THIN_STROKE = new BasicStroke(.7f);
@Override
int getPeriod(int height) {
return Math.max((Registry.is("ide.text.effect.new.metrics") ? height : getMaxHeight(height)) - 1, 1);
}
@Override
void paintImage(Graphics2D g, int width, int height, int period) {
double dx = 0;
double lower = height - 1;
double upper = lower - period;
if (Registry.is("ide.text.effect.new.metrics")) {
if (height > 3) {
float fix = height / 3f;
g.setStroke(new BasicStroke(fix));
if (fix > 1) {
fix = (fix - 1) / 2;
lower -= fix;
upper += fix;
}
}
height += 2;
if (g.getClass().getName().equals("com.intellij.util.HiDPIScaledGraphics")) {
lower += .5;
upper += .5;
}
}
Path2D path = new Path2D.Double();
path.moveTo(dx, lower);
if (height < 6) {
g.setStroke(THIN_STROKE);
while (dx < width) {
path.lineTo(dx += period, upper);
path.lineTo(dx += period, lower);
}
}
else {
double size = (double)period / 2;
double prev = dx - size / 2;
double center = (upper + lower) / 2;
while (dx < width) {
path.quadTo(prev += size, lower, dx += size, center);
path.quadTo(prev += size, upper, dx += size, upper);
path.quadTo(prev += size, upper, dx += size, center);
path.quadTo(prev += size, lower, dx += size, lower);
}
}
path.lineTo((double)width, lower);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.draw(path);
}
};
// we should not recalculate caches when IDEA is on Retina and non-Retina
private final ConcurrentHashMap<Long, BufferedImage> myNormalCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, BufferedImage> myRetinaCache = new ConcurrentHashMap<>();
abstract int getPeriod(int height);
abstract void paintImage(Graphics2D g, int width, int height, int period);
void paintImage(Graphics2D g, Paint paint, int width, int height, int period) {
try {
g.setPaint(paint);
paintImage(g, width, height, period);
}
finally {
g.dispose();
}
}
BufferedImage getImage(Graphics2D g, Color color, int height) {
Long key = color.getRGB() ^ ((long)height << 32);
ConcurrentHashMap<Long, BufferedImage> cache = UIUtil.isRetina(g) ? myRetinaCache : myNormalCache;
return cache.computeIfAbsent(key, k -> createImage(g, color, height));
}
BufferedImage createImage(Graphics2D g, Paint paint, int height) {
int period = getPeriod(height);
int width = period << (paint instanceof Color ? 8 : 1);
BufferedImage image = UIUtil.createImage(g, width, height, BufferedImage.TYPE_INT_ARGB);
paintImage(image.createGraphics(), paint, width, height, period);
return image;
}
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Paint paint) {
if (paint == null) paint = g.getPaint();
g = (Graphics2D)g.create(x, y, width, height);
g.setComposite(AlphaComposite.SrcOver);
BufferedImage image = paint instanceof Color ? getImage(g, (Color)paint, height) : createImage(g, paint, height);
int period = image.getWidth(null);
if (image instanceof JBHiDPIScaledImage) period /= 2;
int offset = (x % period + period) % period; // normalize
for (int dx = -offset; dx < width; dx += period) {
UIUtil.drawImage(g, image, dx, 0, null);
}
g.dispose();
EffectPainter2D.STRIKE_THROUGH.paint(g, x, y, width, height, font);
}
}
}
@@ -0,0 +1,329 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui.paint;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.JBHiDPIScaledImage;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.WavePainter2D;
import java.awt.*;
import java.awt.font.LineMetrics;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Sergey.Malenkov
* @author tav
*/
public enum EffectPainter2D implements RegionPainter2D<Font> {
/**
* @see com.intellij.openapi.editor.markup.EffectType#LINE_UNDERSCORE
*/
LINE_UNDERSCORE {
/**
* Draws a horizontal line under a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height available space under text
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, double x, double y, double width, double height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
LinePainter2D.paint(g, x, y + 1, x + width, y + 1);
}
else {
paintUnderline(g, x, y, width, height, font, 1, this);
}
}
},
/**
* @see com.intellij.openapi.editor.markup.EffectType#BOLD_LINE_UNDERSCORE
*/
BOLD_LINE_UNDERSCORE {
/**
* Draws a bold horizontal line under a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height available space under text
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, double x, double y, double width, double height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
int h = JBUI.scale(Registry.intValue("editor.bold.underline.height", 2));
RectanglePainter2D.FILL.paint(g, x, y, width, h);
}
else {
paintUnderline(g, x, y, width, height, font, 2, this);
}
}
},
/**
* @see com.intellij.openapi.editor.markup.EffectType#BOLD_DOTTED_LINE
*/
BOLD_DOTTED_UNDERSCORE {
/**
* Draws a bold horizontal line of dots under a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height available space under text
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, double x, double y, double width, double height, Font font) {
paintUnderline(g, x, y, width, height, font, 2, this);
}
},
/**
* @see com.intellij.openapi.editor.markup.EffectType#WAVE_UNDERSCORE
*/
WAVE_UNDERSCORE {
/**
* Draws a horizontal wave under a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height available space under text
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, double x, double y, double width, double height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
WavePainter2D.forColor(g.getColor()).paint(g, x, x + width, y + height);
}
else if (Registry.is("ide.text.effect.new.metrics")) {
paintUnderline(g, x, y, width, height, font, 3, this);
}
else if (width > 0 && height > 0) {
Cached.WAVE_UNDERSCORE.paint(g, x, y, width, height, null);
}
}
},
/**
* @see com.intellij.openapi.editor.markup.EffectType#STRIKEOUT
*/
STRIKE_THROUGH {
/**
* Draws a horizontal line through a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height text height
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, double x, double y, double width, double height, Font font) {
if (width > 0 && height > 0) {
if (!Registry.is("ide.text.effect.new.metrics")) {
drawLineCentered(g, x, y - height, width, height, 1, this);
}
else {
if (font == null) font = g.getFont();
LineMetrics metrics = font.getLineMetrics("", g.getFontRenderContext());
double offset = 0.5 - metrics.getStrikethroughOffset();
double thickness = Math.max(1, 0.5 + metrics.getStrikethroughThickness());
drawLine(g, x, y - offset, width, thickness, this);
}
}
}
};
private static int getMaxHeight(double height) {
return height > 7 && Registry.is("ide.text.effect.new.scale") ? (int)height >> 1 : 3;
}
private static void paintUnderline(Graphics2D g, double x, double y, double width, double height, Font font, double thickness, EffectPainter2D painter) {
if (width > 0 && height > 0) {
if (Registry.is("ide.text.effect.new.metrics")) {
if (font == null) font = g.getFont();
LineMetrics metrics = font.getLineMetrics("", g.getFontRenderContext());
thickness = Math.max(thickness, 0.5 + thickness * metrics.getUnderlineThickness());
double offset = Math.min(height - thickness, Math.max(1, 0.5 + metrics.getUnderlineOffset()));
if (offset < 1) {
offset = height > 3 ? 1 : 0;
thickness = height - offset;
}
drawLine(g, x, y + offset, width, thickness, painter);
}
else {
if (height > 3) {
int max = getMaxHeight((int)height);
y += height - max;
height = max;
if (thickness > 1 && height > 3) {
thickness = JBUI.scale((float)thickness);
}
}
drawLineCentered(g, x, y, width, height, thickness, painter);
}
}
}
private static void drawLineCentered(Graphics2D g, double x, double y, double width, double height, double thickness, EffectPainter2D painter) {
int offset = (int)(height - thickness);
if (offset > 0) {
y += offset - (offset >> 1);
height = thickness;
}
drawLine(g, x, y, width, height, painter);
}
private static void drawLine(Graphics2D g, double x, double y, double width, double height, EffectPainter2D painter) {
if (painter == BOLD_DOTTED_UNDERSCORE) {
double dx = (x % height + height) % height;
double w = width + dx;
double dw = (w % height + height) % height;
Cached.BOLD_DOTTED_UNDERSCORE.paint(g, x - dx, y, dw == 0 ? w : w - dw + height, height, null);
}
else if (painter == WAVE_UNDERSCORE) {
Cached.WAVE_UNDERSCORE.paint(g, x, y, width, height, null);
}
else {
RectanglePainter2D.FILL.paint(g, x, y, width, height);
}
}
private enum Cached implements RegionPainter2D<Paint> {
BOLD_DOTTED_UNDERSCORE {
@Override
double getPeriod(double height) {
return height;
}
@Override
void paintImage(Graphics2D g, double width, double height, double period) {
Double round = period <= 2 && !UIUtil.isJreHiDPI(g) ? null : period;
for (int dx = 0; dx < width; dx += period + period) {
RectanglePainter2D.FILL.paint(g, dx, 0, period, period, round, LinePainter2D.StrokeType.INSIDE, 1, RenderingHints.VALUE_ANTIALIAS_DEFAULT);
}
}
},
WAVE_UNDERSCORE {
private final BasicStroke THIN_STROKE = new BasicStroke(.7f);
@Override
double getPeriod(double height) {
return Math.max((Registry.is("ide.text.effect.new.metrics") ? height : getMaxHeight(height)) - 1, 1);
}
@Override
void paintImage(Graphics2D g, double width, double height, double period) {
double dx = 0;
double lower = height - 1;
double upper = lower - period;
if (Registry.is("ide.text.effect.new.metrics")) {
if (height > 3) {
double fix = height / 3;
g.setStroke(new BasicStroke((float)fix));
if (fix > 1) {
fix = (fix - 1) / 2;
lower -= fix;
upper += fix;
}
}
height += 2;
if (g.getClass().getName().equals("com.intellij.util.HiDPIScaledGraphics")) {
lower += .5;
upper += .5;
}
}
Path2D path = new Path2D.Double();
path.moveTo(dx, lower);
if (height < 6) {
g.setStroke(THIN_STROKE);
while (dx < width) {
path.lineTo(dx += period, upper);
path.lineTo(dx += period, lower);
}
}
else {
double size = period / 2;
double prev = dx - size / 2;
double center = (upper + lower) / 2;
while (dx < width) {
path.quadTo(prev += size, lower, dx += size, center);
path.quadTo(prev += size, upper, dx += size, upper);
path.quadTo(prev += size, upper, dx += size, center);
path.quadTo(prev += size, lower, dx += size, lower);
}
}
path.lineTo(width, lower);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.draw(path);
}
};
// we should not recalculate caches when IDEA is on Retina and non-Retina
private final ConcurrentHashMap<Long, BufferedImage> myNormalCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, BufferedImage> myHiDPICache = new ConcurrentHashMap<>();
abstract double getPeriod(double height);
abstract void paintImage(Graphics2D g, double width, double height, double period);
void paintImage(Graphics2D g, Paint paint, double width, double height, double period) {
try {
g.setPaint(paint);
paintImage(g, width, height, period);
}
finally {
g.dispose();
}
}
BufferedImage getImage(Graphics2D g, Color color, double height) {
Long key = color.getRGB() ^ ((long)height << 32);
ConcurrentHashMap<Long, BufferedImage> cache = UIUtil.isJreHiDPI(g) ? myHiDPICache : myNormalCache;
return cache.computeIfAbsent(key, k -> createImage(g, color, height));
}
BufferedImage createImage(Graphics2D g, Paint paint, double height) {
double period = getPeriod(height);
int width = (int)period << (paint instanceof Color ? 8 : 1);
BufferedImage image;
if (UIUtil.isJreHiDPI(g)) {
image = new JBHiDPIScaledImage(g, width, height, BufferedImage.TYPE_INT_ARGB);
} else {
image = UIUtil.createImage(g, width, (int)height, BufferedImage.TYPE_INT_ARGB);
}
paintImage(image.createGraphics(), paint, width, height, period);
return image;
}
@Override
public void paint(Graphics2D g, double x, double y, double width, double height, Paint paint) {
if (paint == null) paint = g.getPaint();
g = (Graphics2D)g.create();
g.translate(x, y);
g.clip(new Rectangle2D.Double(0, 0, width, height));
g.setComposite(AlphaComposite.SrcOver);
BufferedImage image = paint instanceof Color ? getImage(g, (Color)paint, height) : createImage(g, paint, height);
int period = image.getWidth(null);
if (image instanceof JBHiDPIScaledImage) period /= 2;
double offset = (x % period + period) % period; // normalize
g.translate(-offset, 0);
for (double dx = -offset; dx < width; dx += period) {
UIUtil.drawImage(g, image, 0, 0, null);
g.translate(period, 0);
}
g.dispose();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
* Copyright 2000-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,75 +16,23 @@
package com.intellij.util.ui;
import java.awt.*;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
/**
* Draws a 'wavy' line of 1-pixel amplitude. Instances are cached for performance reasons.
* <p/>
* This class is not thread-safe, it's supposed to be used in EDT only.
*
* @see WavePainter2D
*/
public class WavePainter {
private static final float STROKE_WIDTH = 0.7f;
private static final Map<Color, WavePainter> myPainters = new HashMap<Color, WavePainter>();
private static final int PATTERN_WIDTH = 4000;
private final BufferedImage myImage;
private WavePainter(Color color) {
myImage = UIUtil.createImage(PATTERN_WIDTH, 3, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = myImage.createGraphics();
try {
GraphicsUtil.setupAAPainting(g);
g.setStroke(new BasicStroke(STROKE_WIDTH));
g.setColor(color);
double height = 1;
double cycle = 4 * height;
final double wavedAt = 3 - STROKE_WIDTH - height;
GeneralPath wavePath = new GeneralPath();
wavePath.moveTo(0, wavedAt - Math.cos(0 * 2 * Math.PI / cycle) * height);
for (int x = 0; x < PATTERN_WIDTH; x++) {
wavePath.lineTo(x, wavedAt - Math.cos(x * 2 * Math.PI / cycle) * height);
}
g.draw(wavePath);
}
finally {
g.dispose();
}
}
public abstract class WavePainter {
protected WavePainter() {}
/**
* Paints a wave in given coordinate range. {@code y} defines the lower boundary of painted wave.
*/
public void paint(Graphics2D g, int xStart, int xEnd, int y) {
Shape oldClip = g.getClip();
final Rectangle rectangle = new Rectangle(xStart, y - 3, xEnd - xStart, 3);
final Rectangle waveClip = oldClip != null ? oldClip.getBounds().intersection(rectangle) : rectangle;
if (waveClip.isEmpty()) return;
Composite oldComposite = g.getComposite();
try {
g.setComposite(AlphaComposite.SrcOver);
g.setClip(waveClip);
xStart -= xStart % 4;
UIUtil.drawImage(g, myImage, xStart, y - 3, null);
} finally {
g.setComposite(oldComposite);
g.setClip(oldClip);
}
}
public abstract void paint(Graphics2D g, int xStart, int xEnd, int y);
public static WavePainter forColor(Color color) {
WavePainter painter = myPainters.get(color);
if (painter == null) {
painter = new WavePainter(color);
// creating a new Color instance, as the one passed as parameter can be mutable (JBColor) and shouldn't be used as a map key
//noinspection UseJBColor
myPainters.put(new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()), painter);
}
return painter;
return WavePainter2D.forColor(color);
}
}
@@ -0,0 +1,85 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.ui;
import java.awt.*;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
/**
* Draws a 'wavy' line of 1-pixel amplitude. Instances are cached for performance reasons.
* <p/>
* This class is not thread-safe, it's supposed to be used in EDT only.
*/
public class WavePainter2D extends WavePainter {
private static final float STROKE_WIDTH = 0.7f;
private static final Map<Color, WavePainter2D> myPainters = new HashMap<Color, WavePainter2D>();
private static final int PATTERN_WIDTH = 4000;
private final BufferedImage myImage;
private WavePainter2D(Color color) {
myImage = UIUtil.createImage(PATTERN_WIDTH, 3, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = myImage.createGraphics();
try {
GraphicsUtil.setupAAPainting(g);
g.setStroke(new BasicStroke(STROKE_WIDTH));
g.setColor(color);
double height = 1;
double cycle = 4 * height;
final double wavedAt = 3 - STROKE_WIDTH - height;
GeneralPath wavePath = new GeneralPath();
wavePath.moveTo(0, wavedAt - Math.cos(0 * 2 * Math.PI / cycle) * height);
for (int x = 0; x < PATTERN_WIDTH; x++) {
wavePath.lineTo(x, wavedAt - Math.cos(x * 2 * Math.PI / cycle) * height);
}
g.draw(wavePath);
}
finally {
g.dispose();
}
}
/**
* Paints a wave in given coordinate range. {@code y} defines the lower boundary of painted wave.
*/
@Override
public void paint(Graphics2D g, int xStart, int xEnd, int y) {
paint(g, (double)xStart, (double)xEnd, (double)y);
}
/**
* Paints a wave in given coordinate range. {@code y} defines the lower boundary of painted wave.
*/
public void paint(Graphics2D g, double xStart, double xEnd, double y) {
Shape clip = g.getClip();
final Rectangle2D rectangle = new Rectangle2D.Double(xStart, y - 3, xEnd - xStart, 3);
final Rectangle2D waveClip = clip != null ? clip.getBounds2D().createIntersection(rectangle) : rectangle;
if (waveClip.isEmpty()) return;
Graphics2D g2d = (Graphics2D)g.create();
try {
g2d.setComposite(AlphaComposite.SrcOver);
g2d.setClip(waveClip);
xStart -= xStart % 4;
g2d.translate(xStart, y - 3);
UIUtil.drawImage(g2d, myImage, 0, 0, null);
} finally {
g2d.dispose();
}
}
public static WavePainter2D forColor(Color color) {
WavePainter2D painter = myPainters.get(color);
if (painter == null) {
painter = new WavePainter2D(color);
// creating a new Color instance, as the one passed as parameter can be mutable (JBColor) and shouldn't be used as a map key
//noinspection UseJBColor
myPainters.put(new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()), painter);
}
return painter;
}
}