simple stamping

This commit is contained in:
Patrick Haßel 2023-02-23 08:46:11 +01:00
commit 432160302e
23 changed files with 977 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
/data/
/.idea/
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

35
pom.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.ph87.kindermalen</groupId>
<artifactId>KinderMalen</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>com.mortennobel</groupId>
<artifactId>java-image-scaling</artifactId>
<version>0.8.6</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,47 @@
package de.ph87.kindermalen;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.function.Consumer;
public class ClickListener implements MouseListener {
private final Consumer<MouseEvent> onPress;
private final Consumer<MouseEvent> onRelease;
public ClickListener(final Consumer<MouseEvent> onPress, final Consumer<MouseEvent> onRelease) {
this.onPress = onPress;
this.onRelease = onRelease;
}
@Override
public void mouseClicked(final MouseEvent e) {
}
@Override
public void mousePressed(final MouseEvent e) {
if (onPress != null) {
onPress.accept(e);
}
}
@Override
public void mouseReleased(final MouseEvent e) {
if (onRelease != null) {
onRelease.accept(e);
}
}
@Override
public void mouseEntered(final MouseEvent e) {
}
@Override
public void mouseExited(final MouseEvent e) {
}
}

View File

@ -0,0 +1,33 @@
package de.ph87.kindermalen;
import com.mortennobel.imagescaling.ResampleFilters;
import com.mortennobel.imagescaling.ResampleOp;
import java.awt.*;
import java.awt.image.BufferedImage;
public class ImageResizer {
public static BufferedImage RESIZE(final BufferedImage image, final int maxWidth, final int maxHeight) {
final Point size = fitInsideBox(image, maxWidth, maxHeight);
final ResampleOp operation = new ResampleOp(size.x, size.y);
operation.setFilter(ResampleFilters.getLanczos3Filter());
return operation.filter(image, null);
}
private static Point fitInsideBox(final BufferedImage image, final int width, final int height) {
int w = image.getWidth();
int h = image.getHeight();
final double r = (double) w / h;
if (w > width) {
w = width;
h = (int) Math.round(w / r);
}
if (h > width) {
h = height;
w = (int) Math.round(h * r);
}
return new Point(w, h);
}
}

View File

@ -0,0 +1,40 @@
package de.ph87.kindermalen;
import java.awt.event.KeyEvent;
import java.util.function.Consumer;
public class KeyListener implements java.awt.event.KeyListener {
private final boolean ctrl;
private final boolean shift;
private final int keycode;
private final Consumer<KeyEvent> next;
public KeyListener(final boolean ctrl, final boolean shift, final int keycode, final Consumer<KeyEvent> next) {
this.ctrl = ctrl;
this.shift = shift;
this.keycode = keycode;
this.next = next;
}
@Override
public void keyTyped(final KeyEvent e) {
}
@Override
public void keyPressed(final KeyEvent e) {
if (e.isControlDown() == ctrl && e.isShiftDown() == shift && e.getKeyCode() == keycode) {
next.accept(e);
}
}
@Override
public void keyReleased(final KeyEvent e) {
}
}

View File

@ -0,0 +1,117 @@
package de.ph87.kindermalen;
import de.ph87.kindermalen.drawing.Drawing;
import de.ph87.kindermalen.drawing.DrawingPanel;
import de.ph87.kindermalen.tool.Tool;
import de.ph87.kindermalen.tool.ToolPanel;
import de.ph87.kindermalen.tool.stamp.StampTool;
import de.ph87.kindermalen.tool.stamp.StampToolPanel;
import de.ph87.kindermalen.toolbox.ToolBox;
import de.ph87.kindermalen.toolbox.ToolBoxPanel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
public class Main extends JFrame {
public static final int SCREEN = 1;
private static final double SIDEBAR_RATIO = 0.15;
public static final double TOOLBOX_RATIO = 0;
public static final double PROPERTIES_RATIO = 2;
private final ToolBox toolBox = new ToolBox();
private final Drawing drawing = new Drawing(800, 600);
private final DrawingPanel drawingPanel = new DrawingPanel(toolBox, drawing);
private final ToolBoxPanel toolBoxPanel = new ToolBoxPanel(toolBox);
private ToolPanel toolPanel = null;
public static void main(String[] args) {
final Main main = new Main();
main.setVisible(true);
}
public Main() {
setLayout(new GridBagLayout());
final GridBagConstraints c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 0;
c.weightx = SIDEBAR_RATIO;
c.weighty = TOOLBOX_RATIO;
c.fill = GridBagConstraints.BOTH;
c.anchor = GridBagConstraints.FIRST_LINE_START;
add(toolBoxPanel, c);
c.gridx = 1;
c.gridy = 0;
c.weightx = 1 - SIDEBAR_RATIO;
c.weighty = 1;
c.fill = GridBagConstraints.BOTH;
c.gridheight = 3;
add(drawingPanel, c);
pack();
setFullscreen();
setDefaultCloseOperation(EXIT_ON_CLOSE);
addKeyListener(new KeyListener(true, false, KeyEvent.VK_Z, this::undo));
addKeyListener(new KeyListener(true, true, KeyEvent.VK_Z, this::redo));
toolBox.onToolChange(this::onToolChanged);
}
private void undo(final KeyEvent keyEvent) {
drawing.getCurrent().undo();
drawingPanel.repaint();
}
private void redo(final KeyEvent keyEvent) {
drawing.getCurrent().redo();
drawingPanel.repaint();
}
private void onToolChanged(final Tool tool) {
if (toolPanel != null) {
remove(toolPanel);
toolPanel = null;
}
if (tool instanceof StampTool) {
toolPanel = new StampToolPanel((StampTool) tool);
}
if (toolPanel != null) {
final GridBagConstraints c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 1;
c.weightx = SIDEBAR_RATIO;
c.weighty = PROPERTIES_RATIO;
c.fill = GridBagConstraints.BOTH;
c.anchor = GridBagConstraints.FIRST_LINE_START;
add(toolPanel, c);
pack();
}
}
private void setFullscreen() {
final GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
final GraphicsDevice[] graphicsDevices = graphicsEnvironment.getScreenDevices();
if (SCREEN < graphicsDevices.length) {
graphicsDevices[SCREEN].setFullScreenWindow(this);
} else if (graphicsDevices.length > 0) {
graphicsDevices[0].setFullScreenWindow(this);
} else {
throw new RuntimeException("No Screens Found");
}
}
}

View File

@ -0,0 +1,27 @@
package de.ph87.kindermalen;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.function.Consumer;
public class MotionListener implements MouseMotionListener {
private final Consumer<MouseEvent> onDrag;
public MotionListener(final Consumer<MouseEvent> onDrag) {
this.onDrag = onDrag;
}
@Override
public void mouseDragged(final MouseEvent e) {
if (onDrag != null) {
onDrag.accept(e);
}
}
@Override
public void mouseMoved(final MouseEvent e) {
}
}

View File

@ -0,0 +1,44 @@
package de.ph87.kindermalen;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class Publisher<T> {
private final List<Subscription<T>> subscriptions = new ArrayList<>();
private T last = null;
private boolean signalling = false;
public void publish(final T item) {
synchronized (subscriptions) {
if (signalling) {
throw new RuntimeException();
}
signalling = true;
last = item;
new ArrayList<>(subscriptions).forEach(o -> o.next(item));
signalling = false;
}
}
public Subscription<T> subscribe(final Consumer<T> next) {
final Subscription<T> subscription = new Subscription<>(next, this::unsubscribe);
synchronized (subscriptions) {
subscriptions.add(subscription);
if (last != null) {
subscription.next(last);
}
}
return subscription;
}
private void unsubscribe(final Subscription<T> subscription) {
synchronized (subscriptions) {
subscriptions.remove(subscription);
}
}
}

View File

@ -0,0 +1,24 @@
package de.ph87.kindermalen;
import java.util.function.Consumer;
public class Subscription<T> {
private final Consumer<T> next;
private final Consumer<Subscription<T>> teardown;
public Subscription(final Consumer<T> next, final Consumer<Subscription<T>> teardown) {
this.next = next;
this.teardown = teardown;
}
public void unsubscribe() {
teardown.accept(this);
}
public void next(final T item) {
this.next.accept(item);
}
}

View File

@ -0,0 +1,20 @@
package de.ph87.kindermalen.drawing;
import lombok.Getter;
@Getter
public class Drawing {
private final int width;
private final int height;
private final Layer current;
public Drawing(final int width, final int height) {
this.width = width;
this.height = height;
current = new Layer(width, height);
}
}

View File

@ -0,0 +1,64 @@
package de.ph87.kindermalen.drawing;
import de.ph87.kindermalen.ClickListener;
import de.ph87.kindermalen.MotionListener;
import de.ph87.kindermalen.toolbox.ToolBox;
import lombok.extern.slf4j.Slf4j;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
@Slf4j
public class DrawingPanel extends JPanel {
private final ToolBox toolBox;
private final Drawing drawing;
private Vector lastPoint = null;
public DrawingPanel(final ToolBox toolBox, final Drawing drawing) {
this.toolBox = toolBox;
this.drawing = drawing;
this.addMouseListener(new ClickListener(this::onPress, null));
this.addMouseMotionListener(new MotionListener(this::onDrag));
}
private void onPress(final MouseEvent e) {
lastPoint = new Vector(e.getPoint());
drawing.getCurrent().newRevision();
toolBox.getTool().apply(drawing.getCurrent().getCurrent(), e.getX(), e.getY());
repaint();
}
private void onDrag(final MouseEvent e) {
final Vector vector = new Vector(lastPoint, e.getPoint());
int i = 0;
for (int rest = (int) Math.floor(vector.length); rest >= 20; rest -= 20) {
System.out.println(rest);
toolBox.getTool().apply(drawing.getCurrent().getCurrent(), e.getX(), e.getY());
lastPoint = lastPoint.plus(vector.withLength(20));
i++;
if (i > 100) {
break;
}
}
repaint();
}
@Override
public void paint(final Graphics g) {
g.setColor(Color.gray);
g.fillRect(0, 0, getWidth(), getHeight());
if (drawing == null) {
return;
}
g.setColor(Color.white);
g.fillRect(0, 0, drawing.getWidth(), drawing.getHeight());
g.drawImage(drawing.getCurrent().getCurrent(), 0, 0, null);
}
}

View File

@ -0,0 +1,72 @@
package de.ph87.kindermalen.drawing;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class Layer {
private final int width;
private final int height;
@Getter
private BufferedImage current;
private List<BufferedImage> history = new ArrayList<>();
private int index;
public Layer(final int width, final int height) {
this.width = width;
this.height = height;
this.current = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
this.history.add(current);
this.index = 0;
}
public void newRevision() {
if (canRedo()) {
history = history.subList(0, index + 1);
}
current = copy();
index = history.size();
history.add(current);
log.info("Revision {} created", index);
}
private BufferedImage copy() {
final BufferedImage created = new BufferedImage(current.getWidth(), current.getHeight(), current.getType());
current.copyData(created.getRaster());
return created;
}
public void undo() {
if (index > 0) {
index--;
current = history.get(index);
log.info("UNDO: Revision {} loaded", index);
} else {
log.warn("No UNDO steps left.");
}
}
public void redo() {
if (canRedo()) {
index++;
current = history.get(index);
log.info("REDO: Revision {} loaded", index);
} else {
log.warn("No REDO steps left.");
}
}
private boolean canRedo() {
return index < history.size() - 1;
}
}

View File

@ -0,0 +1,36 @@
package de.ph87.kindermalen.drawing;
import java.awt.*;
public class Vector {
public final double x;
public final double y;
public final double length;
public Vector(final Point point) {
this(point.x, point.y);
}
public Vector(final Vector start, final Point end) {
this(end.x - start.x, end.y - start.y);
}
public Vector(final double x, final double y) {
this.x = x;
this.y = y;
this.length = Math.sqrt(x * x + y * y);
}
public Vector withLength(final double newLength) {
final double factor = newLength / length;
return new Vector(x / factor, y / factor);
}
public Vector plus(final Vector other) {
return new Vector(x + other.x, y + other.y);
}
}

View File

@ -0,0 +1,18 @@
package de.ph87.kindermalen.tool;
import lombok.Getter;
import java.awt.image.BufferedImage;
public abstract class Tool {
@Getter
private final String name;
protected Tool(final String name) {
this.name = name;
}
public abstract void apply(final BufferedImage image, final int x, final int y);
}

View File

@ -0,0 +1,18 @@
package de.ph87.kindermalen.tool;
import lombok.extern.slf4j.Slf4j;
import javax.swing.*;
import java.awt.*;
@Slf4j
public abstract class ToolPanel<T extends Tool> extends JPanel {
protected final T tool;
protected ToolPanel(final T tool) {
this.tool = tool;
setPreferredSize(new Dimension(100, 100));
}
}

View File

@ -0,0 +1,18 @@
package de.ph87.kindermalen.tool.stamp;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
public class ListHelper {
public static <A, B> List<B> SYNC(final List<A> aList, final List<B> bOld, final Function<B, A> key, final Function<A, B> create, final Function<B, String> order) {
final List<B> bAdd = aList.stream().filter(a -> bOld.stream().noneMatch(b -> key.apply(b) == a)).map(create).toList();
final List<B> bNew = new ArrayList<>(bOld.stream().filter(panel -> aList.contains(key.apply(panel))).toList());
bNew.addAll(bAdd);
bNew.sort(Comparator.comparing(order));
return bNew;
}
}

View File

@ -0,0 +1,43 @@
package de.ph87.kindermalen.tool.stamp;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static de.ph87.kindermalen.ImageResizer.RESIZE;
@Slf4j
@Getter
@ToString
public class Stamp {
private final String name;
private final File file;
private BufferedImage original = null;
private final Map<Integer, BufferedImage> sizes = new HashMap<>();
public Stamp(final File file) {
this.name = file.getName().substring(0, file.getName().lastIndexOf("."));
this.file = file;
}
public void load() throws IOException {
original = ImageIO.read(file);
log.info("Stamp loaded: {}", this);
}
public BufferedImage getSize(final int size) {
return sizes.computeIfAbsent(size, s -> RESIZE(original, s, s));
}
}

View File

@ -0,0 +1,98 @@
package de.ph87.kindermalen.tool.stamp;
import de.ph87.kindermalen.tool.Tool;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static de.ph87.kindermalen.ImageResizer.RESIZE;
import static de.ph87.kindermalen.tool.stamp.ListHelper.SYNC;
@Slf4j
@ToString
public class StampTool extends Tool {
private static final File STAMPS_DIR = new File("./data/stamps");
@Getter
private List<Stamp> stamps = new ArrayList<>();
@Getter
private Stamp stamp;
@Getter
private int size = 100;
@Getter
private double alpha = 0.75;
private BufferedImage prepared;
public StampTool() {
super("Stempel");
final List<File> files = scan(STAMPS_DIR);
stamps = SYNC(files, stamps, Stamp::getFile, Stamp::new, Stamp::getName);
stamps.stream().filter(stamp -> stamp.getOriginal() == null).forEach(this::load);
}
private List<File> scan(final File dir) {
final List<File> files = new ArrayList<>();
for (final File child : Objects.requireNonNull(dir.listFiles())) {
if (child.isFile()) {
files.add(child.getAbsoluteFile());
} else if (child.isDirectory()) {
files.addAll(scan(child));
}
}
return files;
}
private void load(final Stamp stamp) {
try {
stamp.load();
if (this.stamp == stamp) {
prepare();
}
} catch (IOException e) {
log.error(e.toString());
}
}
public void setStamp(final Stamp stamp) {
this.stamp = stamp;
log.info("Stamp chosen: {}", stamp.getName());
prepare();
}
public void setSize(final int size) {
this.size = size;
prepare();
}
public void setAlpha(final double alpha) {
this.alpha = alpha;
prepare();
}
private void prepare() {
if (this.stamp == null || stamp.getOriginal() == null) {
return;
}
this.prepared = RESIZE(stamp.getOriginal(), size, size);
log.info("Stamp prepared: {}", stamp.getName());
// TODO apply alpha
}
@Override
public void apply(final BufferedImage destination, final int x, final int y) {
destination.getGraphics().drawImage(prepared, x - prepared.getWidth() / 2, y - prepared.getHeight() / 2, null);
}
}

View File

@ -0,0 +1,67 @@
package de.ph87.kindermalen.tool.stamp;
import de.ph87.kindermalen.ClickListener;
import de.ph87.kindermalen.tool.ToolPanel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
@Slf4j
public class StampToolPanel extends ToolPanel<StampTool> {
private static final BasicStroke STROKE_BORDER = new BasicStroke(1f);
private static final int STROKE_HIGHLIGHT_WIDTH = 4;
private static final BasicStroke STROKE_HIGHLIGHT = new BasicStroke(STROKE_HIGHLIGHT_WIDTH);
private static final int SIZE = 50;
public StampToolPanel(final StampTool tool) {
super(tool);
setLayout(new FlowLayout(FlowLayout.LEADING, 0, 0));
tool.getStamps().forEach(stamp -> add(new StampPanel(stamp)));
}
private class StampPanel extends JPanel {
@Getter
private final Stamp stamp;
private final BufferedImage icon;
private final Point position;
public StampPanel(final Stamp stamp) {
this.stamp = stamp;
setPreferredSize(new Dimension(SIZE, SIZE));
addMouseListener(new ClickListener(null, e -> tool.setStamp(stamp)));
icon = stamp.getSize(SIZE - STROKE_HIGHLIGHT_WIDTH * 2);
position = new Point((SIZE - icon.getWidth()) / 2, (SIZE - icon.getHeight()) / 2);
}
@Override
public void paint(final Graphics g) {
super.paint(g);
final Graphics2D g2 = (Graphics2D) g;
if (tool.getStamp() == stamp) {
g2.setColor(Color.orange);
g2.setStroke(STROKE_HIGHLIGHT);
g2.fillRect(0, 0, getWidth(), getHeight());
} else {
g2.setColor(Color.white);
g2.setStroke(STROKE_BORDER);
}
g2.drawRect(0, 0, SIZE - 1, SIZE - 1);
g2.drawImage(icon, position.x, position.y, null);
}
}
}

View File

@ -0,0 +1,7 @@
package de.ph87.kindermalen.toolbox;
public interface Observer<T> {
void next(final T item);
}

View File

@ -0,0 +1,38 @@
package de.ph87.kindermalen.toolbox;
import de.ph87.kindermalen.Publisher;
import de.ph87.kindermalen.Subscription;
import de.ph87.kindermalen.tool.Tool;
import de.ph87.kindermalen.tool.stamp.StampTool;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.function.Consumer;
@Slf4j
public class ToolBox {
@Getter
private final List<Tool> tools = List.of(new StampTool());
@Getter
private Tool tool;
private final Publisher<Tool> onToolChange = new Publisher<>();
public ToolBox() {
setTool(tools.get(0));
}
public void setTool(final Tool tool) {
this.tool = tool;
log.info("Tool selected: {}", tool.getName());
onToolChange.publish(tool);
}
public Subscription<Tool> onToolChange(final Consumer<Tool> next) {
return onToolChange.subscribe(next);
}
}

View File

@ -0,0 +1,56 @@
package de.ph87.kindermalen.toolbox;
import de.ph87.kindermalen.ClickListener;
import de.ph87.kindermalen.tool.Tool;
import lombok.extern.slf4j.Slf4j;
import javax.swing.*;
import java.awt.*;
@Slf4j
public class ToolBoxPanel extends JPanel {
private final ToolBox toolbox;
public ToolBoxPanel(final ToolBox toolbox) {
this.toolbox = toolbox;
setLayout(new FlowLayout(FlowLayout.LEADING, 0, 0));
for (final Tool tool : toolbox.getTools()) {
add(new ToolButton(tool));
}
}
@Override
public void paint(final Graphics g) {
super.paint(g);
// final Graphics2D g2 = (Graphics2D) g;
// g2.setColor(Color.red);
// g2.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{2f}, 0));
// g2.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
}
private class ToolButton extends Component {
private final Tool tool;
public ToolButton(final Tool tool) {
this.tool = tool;
setPreferredSize(new Dimension(50, 50));
addMouseListener(new ClickListener(null, e -> toolbox.setTool(tool)));
}
@Override
public void paint(final Graphics g) {
g.setColor(Color.magenta);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(Color.black);
g.setFont(g.getFont().deriveFont(12.0f));
g.drawString(tool.getName(), 0, g.getFontMetrics().getHeight());
g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
}
}
}

View File

@ -0,0 +1,13 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{15}) -- %msg %n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
</configuration>