mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
Monochromatic mode
This commit is contained in:
@@ -68,5 +68,9 @@ public enum DrawOption {
|
||||
/**
|
||||
* Whether locked components should be rendered as transparent or not.
|
||||
*/
|
||||
LOCKED_ALPHA;
|
||||
LOCKED_ALPHA,
|
||||
/**
|
||||
* When applied, all colors will be converted to blackest black or whitest white
|
||||
*/
|
||||
MONOCHROMATIC;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.diylc.presenter;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.List;
|
||||
|
||||
@@ -71,4 +72,52 @@ public class CalcUtils {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the relative luminance (perceived brightness) of a color.
|
||||
* Uses the standard formula: L = 0.299*R + 0.587*G + 0.114*B
|
||||
*
|
||||
* @param color the color to calculate luminance for
|
||||
* @return the luminance value between 0 and 255
|
||||
*/
|
||||
public static double calculateLuminance(Color color) {
|
||||
if (color == null) {
|
||||
return 0.0;
|
||||
}
|
||||
return 0.299 * color.getRed() + 0.587 * color.getGreen() + 0.114 * color.getBlue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a color to monochrome (black or white) based on its luminance.
|
||||
* Colors with luminance below the threshold (128) are converted to black,
|
||||
* otherwise to white. The alpha channel is preserved if present.
|
||||
*
|
||||
* @param color the color to convert
|
||||
* @param threshold the luminance threshold (default 128 for middle gray)
|
||||
* @return black or white color, preserving alpha channel
|
||||
*/
|
||||
public static Color convertToMonochrome(Color color, double threshold) {
|
||||
if (color == null) {
|
||||
return null;
|
||||
}
|
||||
double luminance = calculateLuminance(color);
|
||||
Color monochromeColor = (luminance < threshold) ? Color.BLACK : Color.WHITE;
|
||||
// Preserve alpha channel if present
|
||||
if (color.getAlpha() < 255) {
|
||||
monochromeColor = new Color(monochromeColor.getRed(), monochromeColor.getGreen(),
|
||||
monochromeColor.getBlue(), color.getAlpha());
|
||||
}
|
||||
return monochromeColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a color to monochrome (black or white) based on its luminance.
|
||||
* Uses the default threshold of 128 (middle gray).
|
||||
*
|
||||
* @param color the color to convert
|
||||
* @return black or white color, preserving alpha channel
|
||||
*/
|
||||
public static Color convertToMonochrome(Color color) {
|
||||
return convertToMonochrome(color, 128.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,12 @@ import java.lang.ref.SoftReference;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import org.diylc.common.ComponentType;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.core.ComponentForRender;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
@@ -67,14 +69,14 @@ public class DrawingCache {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void bulkPrepare(Collection<ComponentForRender> components, G2DWrapper g2d, boolean outlineMode, Project project,
|
||||
double zoom, double scaleFactor) {
|
||||
double zoom, double scaleFactor, Set<DrawOption> drawOptions) {
|
||||
// run all renders in parallel to utilize all CPU cores
|
||||
components.stream().parallel().forEach(x -> {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) x.getComponent().getClass());
|
||||
|
||||
if (type != null && type.getEnableCache()) {
|
||||
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder());
|
||||
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder(), drawOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -92,7 +94,7 @@ public class DrawingCache {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void draw(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Rectangle2D visibleRect) {
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Rectangle2D visibleRect, Set<DrawOption> drawOptions) {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
|
||||
|
||||
@@ -100,7 +102,7 @@ public class DrawingCache {
|
||||
// if we need to apply caching
|
||||
Point2D firstPoint = component.getControlPoint(0);
|
||||
|
||||
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder);
|
||||
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder, drawOptions);
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
@@ -180,7 +182,7 @@ public class DrawingCache {
|
||||
}
|
||||
|
||||
private CacheValue renderAndCache(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder) {
|
||||
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Set<DrawOption> drawOptions) {
|
||||
|
||||
if (!g2d.getClipBounds().intersects(component.getCachingBounds())) {
|
||||
return null;
|
||||
@@ -199,9 +201,11 @@ public class DrawingCache {
|
||||
}
|
||||
|
||||
// only honor the cache if the component hasn't changed in the meantime
|
||||
// Also check if drawOptions changed (e.g., MONOCHROMATIC affects rendering)
|
||||
boolean drawOptionsChanged = value == null || !value.getDrawOptions().equals(drawOptions);
|
||||
if (value == null || !value.getComponent().equalsTo(component)
|
||||
|| value.getState() != componentState || value.getZoom() != zoom || value.getOutlineMode() != outlineMode
|
||||
|| value.getScaleFactor() != scaleFactor) {
|
||||
|| value.getScaleFactor() != scaleFactor || drawOptionsChanged) {
|
||||
// figure out size and placement of buffer image
|
||||
Rectangle2D rect = component.getCachingBounds();
|
||||
int width = (int)Math.round(rect.getWidth() * zoom);
|
||||
@@ -233,7 +237,7 @@ public class DrawingCache {
|
||||
cg2d.translate(-firstPoint.getX() + dx, -firstPoint.getY() + dy);
|
||||
|
||||
// wrap the graphics so we can track
|
||||
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom);
|
||||
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom, drawOptions);
|
||||
|
||||
// initialize wrapper
|
||||
wrapper.startedDrawingComponent(zOrder);
|
||||
@@ -275,7 +279,7 @@ public class DrawingCache {
|
||||
|
||||
// add to the cache
|
||||
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
|
||||
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect);
|
||||
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect, drawOptions);
|
||||
imageCache.put(component, new SoftReference<DrawingCache.CacheValue>(value));
|
||||
}
|
||||
|
||||
@@ -295,11 +299,12 @@ public class DrawingCache {
|
||||
int dx;
|
||||
int dy;
|
||||
Rectangle2D cacheBounds;
|
||||
Set<DrawOption> drawOptions;
|
||||
|
||||
public CacheValue(IDIYComponent<?> component, BufferedImage image, Area currentArea,
|
||||
Map<String, Area> continuityPositiveAreas, Map<String, Area> continuityNegativeAreas,
|
||||
ComponentState state, boolean outlineMode, double zoom, double scaleFactor, int dx, int dy,
|
||||
Rectangle2D cacheBounds) {
|
||||
Rectangle2D cacheBounds, Set<DrawOption> drawOptions) {
|
||||
super();
|
||||
this.cacheBounds = cacheBounds;
|
||||
// take a copy of the component, so we can check if it changed in the meantime
|
||||
@@ -317,6 +322,7 @@ public class DrawingCache {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this.scaleFactor = scaleFactor;
|
||||
this.drawOptions = drawOptions;
|
||||
}
|
||||
|
||||
public IDIYComponent<?> getComponent() {
|
||||
@@ -366,6 +372,10 @@ public class DrawingCache {
|
||||
public Rectangle2D getCacheBounds() {
|
||||
return cacheBounds;
|
||||
}
|
||||
|
||||
public Set<DrawOption> getDrawOptions() {
|
||||
return drawOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -184,7 +184,7 @@ public class DrawingManager {
|
||||
if (externalZoom != null)
|
||||
zoom *= externalZoom;
|
||||
|
||||
G2DWrapper g2dWrapper = new G2DWrapper(g2d, zoom);
|
||||
G2DWrapper g2dWrapper = new G2DWrapper(g2d, zoom, drawOptions);
|
||||
|
||||
configureRenderingHints(g2d, drawOptions);
|
||||
|
||||
@@ -268,7 +268,7 @@ public class DrawingManager {
|
||||
|
||||
// give the cache a chance to prepare all the components in multiple threads
|
||||
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) {
|
||||
DrawingCache.Instance.bulkPrepare(componentForRenderList, g2dWrapper, outlineMode, project, zoom, scaleFactor);
|
||||
DrawingCache.Instance.bulkPrepare(componentForRenderList, g2dWrapper, outlineMode, project, zoom, scaleFactor, drawOptions);
|
||||
}
|
||||
|
||||
// componentAreaMap.clear();
|
||||
@@ -562,10 +562,12 @@ public class DrawingManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor, Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state, double zoom, int i, boolean outlineMode) {
|
||||
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor,
|
||||
Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state,
|
||||
double zoom, int i, boolean outlineMode) {
|
||||
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache
|
||||
DrawingCache.Instance.draw(component, g2dWrapper, state,
|
||||
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect);
|
||||
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect, drawOptions);
|
||||
else // go straight to the wrapper
|
||||
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
|
||||
}
|
||||
|
||||
@@ -53,17 +53,11 @@ import java.awt.image.renderable.RenderableImage;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.text.AttributedCharacterIterator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
import org.diylc.common.DrawOption;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.common.ZoomableStroke;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
@@ -96,7 +90,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
private int uniqueCounter = 0;
|
||||
|
||||
private Graphics2D canvasGraphics;
|
||||
private final Graphics2D canvasGraphics;
|
||||
private final Set<DrawOption> drawOptions;
|
||||
|
||||
private Stroke originalStroke;
|
||||
private Color originalColor;
|
||||
private Composite originalComposite;
|
||||
@@ -115,12 +111,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
/**
|
||||
* Creates a wrapper around specified {@link Graphics2D} object.
|
||||
*
|
||||
*
|
||||
* @param canvasGraphics
|
||||
* @param drawOptions
|
||||
*/
|
||||
public G2DWrapper(Graphics2D canvasGraphics, double zoom) {
|
||||
public G2DWrapper(Graphics2D canvasGraphics, double zoom, Set<DrawOption> drawOptions) {
|
||||
super();
|
||||
this.canvasGraphics = canvasGraphics;
|
||||
this.drawOptions = drawOptions;
|
||||
String debugAreaPerformanceStr = System.getProperty(DEBUG_AREA_PERFORMANCE);
|
||||
debugAreaPerformance = "true".equalsIgnoreCase(debugAreaPerformanceStr);
|
||||
this.zoom = zoom;
|
||||
@@ -758,7 +756,11 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
|
||||
|
||||
@Override
|
||||
public void setColor(Color c) {
|
||||
canvasGraphics.setColor(c);
|
||||
if (drawOptions.contains(DrawOption.MONOCHROMATIC) && c != null) {
|
||||
canvasGraphics.setColor(CalcUtils.convertToMonochrome(c));
|
||||
} else {
|
||||
canvasGraphics.setColor(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.diylc.presenter;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -142,4 +143,107 @@ public class CalcUtilsTest {
|
||||
point2.setLocation(10.0, 21.0);
|
||||
assertFalse(CalcUtils.pointsMatch(point1, point2, 0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateLuminance() {
|
||||
// Test pure black
|
||||
assertEquals(0.0, CalcUtils.calculateLuminance(Color.BLACK), 0.001);
|
||||
|
||||
// Test pure white
|
||||
assertEquals(255.0, CalcUtils.calculateLuminance(Color.WHITE), 0.001);
|
||||
|
||||
// Test pure red
|
||||
double redLuminance = CalcUtils.calculateLuminance(Color.RED);
|
||||
assertEquals(76.245, redLuminance, 0.001); // 0.299 * 255
|
||||
|
||||
// Test pure green
|
||||
double greenLuminance = CalcUtils.calculateLuminance(Color.GREEN);
|
||||
assertEquals(149.685, greenLuminance, 0.001); // 0.587 * 255
|
||||
|
||||
// Test pure blue
|
||||
double blueLuminance = CalcUtils.calculateLuminance(Color.BLUE);
|
||||
assertEquals(29.07, blueLuminance, 0.001); // 0.114 * 255
|
||||
|
||||
// Test middle gray (128, 128, 128)
|
||||
Color gray = new Color(128, 128, 128);
|
||||
double grayLuminance = CalcUtils.calculateLuminance(gray);
|
||||
assertEquals(128.0, grayLuminance, 0.001);
|
||||
|
||||
// Test null color
|
||||
assertEquals(0.0, CalcUtils.calculateLuminance(null), 0.001);
|
||||
|
||||
// Test custom color
|
||||
Color custom = new Color(100, 150, 200);
|
||||
double customLuminance = CalcUtils.calculateLuminance(custom);
|
||||
double expected = 0.299 * 100 + 0.587 * 150 + 0.114 * 200;
|
||||
assertEquals(expected, customLuminance, 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToMonochrome() {
|
||||
// Test pure black -> should stay black
|
||||
Color result = CalcUtils.convertToMonochrome(Color.BLACK);
|
||||
assertEquals(Color.BLACK, result);
|
||||
|
||||
// Test pure white -> should stay white
|
||||
result = CalcUtils.convertToMonochrome(Color.WHITE);
|
||||
assertEquals(Color.WHITE, result);
|
||||
|
||||
// Test dark color -> should become black
|
||||
Color darkBlue = new Color(0, 0, 50);
|
||||
result = CalcUtils.convertToMonochrome(darkBlue);
|
||||
assertEquals(Color.BLACK, result);
|
||||
|
||||
// Test light color -> should become white
|
||||
Color lightYellow = new Color(255, 255, 200);
|
||||
result = CalcUtils.convertToMonochrome(lightYellow);
|
||||
assertEquals(Color.WHITE, result);
|
||||
|
||||
// Test middle gray -> should become white (threshold is 128, gray is exactly 128)
|
||||
Color gray = new Color(128, 128, 128);
|
||||
result = CalcUtils.convertToMonochrome(gray);
|
||||
assertEquals(Color.WHITE, result);
|
||||
|
||||
// Test color just below threshold -> should become black
|
||||
Color darkGray = new Color(127, 127, 127);
|
||||
result = CalcUtils.convertToMonochrome(darkGray);
|
||||
assertEquals(Color.BLACK, result);
|
||||
|
||||
// Test color just above threshold -> should become white
|
||||
Color lightGray = new Color(129, 129, 129);
|
||||
result = CalcUtils.convertToMonochrome(lightGray);
|
||||
assertEquals(Color.WHITE, result);
|
||||
|
||||
// Test with custom threshold
|
||||
Color mediumColor = new Color(100, 100, 100);
|
||||
result = CalcUtils.convertToMonochrome(mediumColor, 50.0);
|
||||
assertEquals(Color.WHITE, result); // Luminance ~100, threshold 50, so white
|
||||
|
||||
result = CalcUtils.convertToMonochrome(mediumColor, 150.0);
|
||||
assertEquals(Color.BLACK, result); // Luminance ~100, threshold 150, so black
|
||||
|
||||
// Test null color
|
||||
assertNull(CalcUtils.convertToMonochrome(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertToMonochromeWithAlpha() {
|
||||
// Test color with alpha channel - alpha should be preserved
|
||||
Color transparentRed = new Color(255, 0, 0, 128);
|
||||
Color result = CalcUtils.convertToMonochrome(transparentRed);
|
||||
assertEquals(Color.WHITE.getRed(), result.getRed());
|
||||
assertEquals(Color.WHITE.getGreen(), result.getGreen());
|
||||
assertEquals(Color.WHITE.getBlue(), result.getBlue());
|
||||
assertEquals(128, result.getAlpha());
|
||||
|
||||
// Test fully transparent color
|
||||
Color transparent = new Color(100, 150, 200, 0);
|
||||
result = CalcUtils.convertToMonochrome(transparent);
|
||||
assertEquals(0, result.getAlpha());
|
||||
|
||||
// Test opaque color - alpha should remain 255
|
||||
Color opaque = new Color(50, 50, 50);
|
||||
result = CalcUtils.convertToMonochrome(opaque);
|
||||
assertEquals(255, result.getAlpha());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ public class SilkScreenDrawingProvider implements IDrawingProvider {
|
||||
|
||||
@Override
|
||||
public void draw(int page, Graphics g, double zoomFactor) {
|
||||
plugInPort.draw((Graphics2D) g, EnumSet.of(DrawOption.ANTIALIASING, DrawOption.OUTLINE_MODE),
|
||||
plugInPort.draw((Graphics2D) g, EnumSet.of(DrawOption.ANTIALIASING, DrawOption.OUTLINE_MODE, DrawOption.MONOCHROMATIC),
|
||||
new SilkScreenFilter(), zoomFactor, null, null);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user