diff --git a/pom.xml b/pom.xml index 16698d9..5e4fbc7 100644 --- a/pom.xml +++ b/pom.xml @@ -122,5 +122,6 @@ jcodec-javase 0.2.5 + \ No newline at end of file diff --git a/src/main/java/io/swtc/Main.java b/src/main/java/io/swtc/Main.java index 80cd92f..a5b5d3e 100644 --- a/src/main/java/io/swtc/Main.java +++ b/src/main/java/io/swtc/Main.java @@ -1,11 +1,29 @@ package io.swtc; +import javax.swing.*; + public class Main { public static void main(String[] args) { for (int i = 0; i < args.length; i++) { System.out.println("Arg " + i + ": " + args[i]); } + + try { + //UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel"); + UIManager.setLookAndFeel( + "com.sun.java.swing.plaf.windows.WindowsLookAndFeel" + ); + } catch (Exception e) { + JOptionPane.showMessageDialog( + null, + "Method not supported, Application will run anyways but might look different, and feel different!", + "SWTCCTVWarning", + JOptionPane.WARNING_MESSAGE + ); + } + + SwingCCTVManager.main(null); } } diff --git a/src/main/java/io/swtc/SwingCameraWindow.java b/src/main/java/io/swtc/SwingCameraWindow.java index de3af66..0ac69fc 100644 --- a/src/main/java/io/swtc/SwingCameraWindow.java +++ b/src/main/java/io/swtc/SwingCameraWindow.java @@ -9,6 +9,7 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; +@Deprecated public class SwingCameraWindow { private final JFrame frame; private final CameraPanel cameraPanel; diff --git a/src/main/java/io/swtc/SwingIFrame.java b/src/main/java/io/swtc/SwingIFrame.java index f2f76b2..1b4416b 100644 --- a/src/main/java/io/swtc/SwingIFrame.java +++ b/src/main/java/io/swtc/SwingIFrame.java @@ -3,44 +3,45 @@ package io.swtc; import com.github.sarxos.webcam.Webcam; import io.swtc.proccessing.WebcamCaptureLoop; import io.swtc.proccessing.CameraPanel; +import io.swtc.proccessing.FilterPanel; import io.swtc.recording.VideoRecorder; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.ActionEvent; +import java.awt.geom.CubicCurve2D; import java.awt.image.BufferedImage; import java.io.File; import java.text.SimpleDateFormat; -import java.util.Date; - -/* -* -* This file is basically just UI, its boring the interesting stuff is in the utilities section! -* -* */ +import java.util.*; +/** + * Blender-style UI where the Effects panel is triggered by a Tab click + */ public class SwingIFrame { private final JFrame mainFrame; - private final JDesktopPane desktopPane; + private final BlenderDesktopPane desktopPane; private boolean fullscreen = false; private Rectangle windowedBounds; private boolean blackbg = false; private final Color defDesktopBg; private final Color bgcolor; - public SwingIFrame() { - mainFrame = new JFrame("viewer"); - mainFrame.setSize(1280,720); - mainFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + private final Map cameraToEffects = new HashMap<>(); + + public SwingIFrame() { + mainFrame = new JFrame("Viewer"); + mainFrame.setSize(1280, 720); + mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - // this is good on the eyes for long periods of times, - // i would have used #4B9EA0, which is also easy on the eyes bgcolor = Color.decode("#1e1e1e"); - desktopPane = new JDesktopPane(); + desktopPane = new BlenderDesktopPane(cameraToEffects); desktopPane.setBackground(Color.WHITE); defDesktopBg = desktopPane.getBackground(); mainFrame.add(desktopPane, BorderLayout.CENTER); @@ -50,7 +51,7 @@ public class SwingIFrame { } public void addCameraInternalFrame(Webcam webcam) { - JInternalFrame iframe = new JInternalFrame( + JInternalFrame cameraFrame = new JInternalFrame( webcam.getName(), true, true, true, true ); @@ -61,326 +62,152 @@ public class SwingIFrame { ); JPanel contentPanel = new JPanel(new BorderLayout()); - JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.addTab("View", cameraPanel); tabbedPane.addTab("Capture", createCapturePanel(cameraPanel, webcam)); - tabbedPane.addTab("Effects", createEffectsPanel(cameraPanel)); + // Add a placeholder tab that acts as a button + tabbedPane.addTab("Open Effect Panel", new JPanel()); contentPanel.add(tabbedPane, BorderLayout.CENTER); - iframe.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() { + // Logic to open/show the Effects Frame when the tab is clicked + tabbedPane.addChangeListener(new ChangeListener() { @Override - public void internalFrameClosing(javax.swing.event.InternalFrameEvent e) { - // if we dont call this the camera stays open until the procces dies. - captureLoop.stop(); + public void stateChanged(ChangeEvent e) { + if (tabbedPane.getSelectedIndex() == 2) { // "Effects +" tab index + // Revert selection back to "View" so the tab doesn't stay stuck on the empty panel + tabbedPane.setSelectedIndex(0); + + EffectsPanelFrame effectsFrame = cameraToEffects.get(cameraFrame); + if (effectsFrame != null) { + effectsFrame.setVisible(true); + try { + effectsFrame.setSelected(true); + } catch (java.beans.PropertyVetoException ex) { + ex.printStackTrace(); + } + } + } } }); - iframe.add(contentPanel); - iframe.setSize(600, 500); + cameraFrame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() { + @Override + public void internalFrameClosing(javax.swing.event.InternalFrameEvent e) { + captureLoop.stop(); + EffectsPanelFrame effectsFrame = cameraToEffects.get(cameraFrame); + if (effectsFrame != null) { + effectsFrame.dispose(); + cameraToEffects.remove(cameraFrame); + } + } + }); + + cameraFrame.add(contentPanel); + cameraFrame.setSize(600, 500); int offset = desktopPane.getAllFrames().length * 30; - iframe.setLocation(offset, offset); + cameraFrame.setLocation(50 + offset, 50 + offset); - desktopPane.add(iframe); - iframe.setVisible(true); + // Pre-create the effects frame but keep it hidden + EffectsPanelFrame effectsFrame = new EffectsPanelFrame( + "Effects - " + webcam.getName(), + cameraPanel, + cameraFrame + ); + effectsFrame.setSize(350, 600); + effectsFrame.setLocation(700 + offset, 50 + offset); + effectsFrame.setVisible(false); // Hidden by default + + cameraToEffects.put(cameraFrame, effectsFrame); + addLinkageListeners(cameraFrame, effectsFrame); + + desktopPane.add(cameraFrame); + desktopPane.add(effectsFrame); + + cameraFrame.setVisible(true); captureLoop.start(); } + private void addLinkageListeners(JInternalFrame cameraFrame, EffectsPanelFrame effectsFrame) { + java.awt.event.ComponentAdapter linker = new java.awt.event.ComponentAdapter() { + @Override + public void componentMoved(java.awt.event.ComponentEvent e) { + desktopPane.repaint(); + } + @Override + public void componentResized(java.awt.event.ComponentEvent e) { + desktopPane.repaint(); + } + }; + cameraFrame.addComponentListener(linker); + effectsFrame.addComponentListener(linker); + } + + // --- Standard UI Setup Methods --- + private void setupBlackBg() { JRootPane root = mainFrame.getRootPane(); - - root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(KeyStroke.getKeyStroke("B"), "toggleBlackBg"); - + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("B"), "toggleBlackBg"); root.getActionMap().put("toggleBlackBg", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - setbgblack(); - } + @Override public void actionPerformed(ActionEvent e) { setbgblack(); } }); } - /* Setup F11 for Fullscreen */ + private void setupFullscreenToggle() { JRootPane root = mainFrame.getRootPane(); - - root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(KeyStroke.getKeyStroke("F11"), "toggleFullscreen"); - + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("F11"), "toggleFullscreen"); root.getActionMap().put("toggleFullscreen", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - toggleFullscreen(); - } + @Override public void actionPerformed(ActionEvent e) { toggleFullscreen(); } }); } private void setbgblack() { if (!blackbg) { - // easy stuff here, just setting the bg to the private color desktopPane.setBackground(bgcolor); - updateInternalFrameBg(bgcolor); } else { desktopPane.setBackground(defDesktopBg); - updateInternalFrameBg(null); } blackbg = !blackbg; } - private void updateInternalFrameBg(Color bg) { - for (JInternalFrame frame : desktopPane.getAllFrames()) { - Container content = frame.getContentPane(); - if (bg != null) { - content.setBackground(bg); - } else content.setBackground(null); // restore default - content.repaint(); - } - } - private void toggleFullscreen() { if (!fullscreen) { - // We set the window to borderless windowed mode, so it doesnt - // lag like shit when we drag windows around, which is annoying windowedBounds = mainFrame.getBounds(); mainFrame.dispose(); mainFrame.setUndecorated(true); mainFrame.setExtendedState(JFrame.MAXIMIZED_BOTH); mainFrame.setVisible(true); } else { - // do the opposite mainFrame.dispose(); mainFrame.setUndecorated(false); mainFrame.setExtendedState(JFrame.NORMAL); mainFrame.setBounds(windowedBounds); mainFrame.setVisible(true); } - fullscreen = !fullscreen; } - - private JPanel createCapturePanel(CameraPanel cameraPanel, Webcam webcam) { JPanel panel = new JPanel(new BorderLayout(10, 10)); panel.setBorder(new EmptyBorder(15, 15, 15, 15)); - JPanel mainPanel = new JPanel(); - mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); - - // Screenshot section - JPanel screenshotPanel = new JPanel(); - screenshotPanel.setLayout(new BoxLayout(screenshotPanel, BoxLayout.Y_AXIS)); - screenshotPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Screenshot", - TitledBorder.LEFT, TitledBorder.TOP)); - - JPanel screenshotPathPanel = new JPanel(new BorderLayout(5, 5)); - JTextField screenshotPath = new JTextField(System.getProperty("user.home") + File.separator + "screenshots"); - JButton browseSS = new JButton("..."); - browseSS.setPreferredSize(new Dimension(40, 25)); - browseSS.addActionListener(e -> browseDirectory(screenshotPath, panel)); - screenshotPathPanel.add(screenshotPath, BorderLayout.CENTER); - screenshotPathPanel.add(browseSS, BorderLayout.EAST); - - JButton takeScreenshot = new JButton("Take Screenshot (S)"); - takeScreenshot.setAlignmentX(Component.CENTER_ALIGNMENT); - takeScreenshot.addActionListener(e -> saveSnapshot(cameraPanel, webcam, screenshotPath.getText(), panel)); - - screenshotPanel.add(screenshotPathPanel); - screenshotPanel.add(Box.createRigidArea(new Dimension(0, 10))); - screenshotPanel.add(takeScreenshot); - - // Recording section - JPanel recordPanel = new JPanel(); - recordPanel.setLayout(new BoxLayout(recordPanel, BoxLayout.Y_AXIS)); - recordPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Recording", - TitledBorder.LEFT, TitledBorder.TOP)); - - JPanel recordPathPanel = new JPanel(new BorderLayout(5, 5)); - JTextField recordPath = new JTextField(System.getProperty("user.home") + File.separator + "recordings"); - JButton browseRec = new JButton("..."); - browseRec.setPreferredSize(new Dimension(40, 25)); - browseRec.addActionListener(e -> browseDirectory(recordPath, panel)); - recordPathPanel.add(recordPath, BorderLayout.CENTER); - recordPathPanel.add(browseRec, BorderLayout.EAST); - - VideoRecorder recorder = new VideoRecorder(); - JButton recordButton = new JButton("Start Recording (R)"); - JLabel recordingStatus = new JLabel("Ready"); - recordingStatus.setAlignmentX(Component.CENTER_ALIGNMENT); - - recordButton.setAlignmentX(Component.CENTER_ALIGNMENT); - recordButton.addActionListener(e -> { - if (!recorder.isRecording()) { - String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String filename = "recording_" + webcam.getName().replaceAll("[^a-zA-Z0-9]", "_") - + "_" + timestamp + ".mp4"; - File dir = new File(recordPath.getText()); - dir.mkdirs(); - - try { - recorder.startRecording(cameraPanel, new File(dir, filename)); - recordButton.setText("Stop Recording"); - recordingStatus.setText("Recording..."); - recordingStatus.setForeground(Color.RED); - } catch (Exception ex) { - JOptionPane.showMessageDialog(panel, - "Error starting recording: " + ex.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - } else { - try { - File saved = recorder.stopRecording(); - recordButton.setText("Start Recording (R)"); - recordingStatus.setText("Saved: " + saved.getName()); - recordingStatus.setForeground(Color.BLACK); - } catch (Exception ex) { - recordButton.setText("Start Recording (R)"); - recordingStatus.setText("Error saving"); - recordingStatus.setForeground(Color.RED); - JOptionPane.showMessageDialog(panel, - "Error saving recording: " + ex.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - } - }); - - recordPanel.add(recordPathPanel); - recordPanel.add(Box.createRigidArea(new Dimension(0, 10))); - recordPanel.add(recordButton); - recordPanel.add(Box.createRigidArea(new Dimension(0, 5))); - recordPanel.add(recordingStatus); - - mainPanel.add(screenshotPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 15))); - mainPanel.add(recordPanel); - mainPanel.add(Box.createVerticalGlue()); - - panel.add(mainPanel, BorderLayout.NORTH); + JButton takeScreenshot = new JButton("Take Screenshot"); + takeScreenshot.addActionListener(e -> saveSnapshot(cameraPanel, webcam, System.getProperty("user.home"), panel)); + panel.add(takeScreenshot, BorderLayout.NORTH); return panel; } - private JPanel createEffectsPanel(CameraPanel cameraPanel) { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(new EmptyBorder(15, 15, 15, 15)); - - JPanel transformPanel = new JPanel(new GridLayout(0, 1, 5, 5)); - transformPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Transform", - TitledBorder.LEFT, TitledBorder.TOP)); - - JCheckBox mirrorCheck = new JCheckBox("Mirror Horizontal"); - JCheckBox flipCheck = new JCheckBox("Flip Vertical"); - JCheckBox rotateCheck = new JCheckBox("Rotate 180°"); - - mirrorCheck.addActionListener(e -> cameraPanel.setMirror(mirrorCheck.isSelected())); - flipCheck.addActionListener(e -> cameraPanel.setFlip(flipCheck.isSelected())); - rotateCheck.addActionListener(e -> cameraPanel.setRotate(rotateCheck.isSelected())); - - transformPanel.add(mirrorCheck); - transformPanel.add(flipCheck); - transformPanel.add(rotateCheck); - - JPanel colorPanel = new JPanel(); - colorPanel.setLayout(new BoxLayout(colorPanel, BoxLayout.Y_AXIS)); - colorPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Color", - TitledBorder.LEFT, TitledBorder.TOP)); - - JCheckBox grayscaleCheck = new JCheckBox("Grayscale"); - JCheckBox invertCheck = new JCheckBox("Invert Colors"); - - grayscaleCheck.addActionListener(e -> cameraPanel.setGrayscale(grayscaleCheck.isSelected())); - invertCheck.addActionListener(e -> cameraPanel.setInvert(invertCheck.isSelected())); - - colorPanel.add(grayscaleCheck); - colorPanel.add(Box.createRigidArea(new Dimension(0, 5))); - colorPanel.add(invertCheck); - - JPanel brightnessPanel = new JPanel(); - brightnessPanel.setLayout(new BoxLayout(brightnessPanel, BoxLayout.Y_AXIS)); - brightnessPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Adjustments", - TitledBorder.LEFT, TitledBorder.TOP)); - - JPanel brightnessSliderPanel = new JPanel(new BorderLayout()); - JLabel brightnessLabel = new JLabel("Brightness: 0"); - JSlider brightnessSlider = new JSlider(-100, 100, 0); - brightnessSlider.addChangeListener(e -> { - int value = brightnessSlider.getValue(); - brightnessLabel.setText("Brightness: " + value); - cameraPanel.setBrightness(value); - }); - brightnessSliderPanel.add(brightnessLabel, BorderLayout.NORTH); - brightnessSliderPanel.add(brightnessSlider, BorderLayout.CENTER); - - JPanel contrastSliderPanel = new JPanel(new BorderLayout()); - JLabel contrastLabel = new JLabel("Contrast: 1.0"); - JSlider contrastSlider = new JSlider(0, 200, 100); - contrastSlider.addChangeListener(e -> { - float value = contrastSlider.getValue() / 100f; - contrastLabel.setText("Contrast: " + String.format("%.1f", value)); - cameraPanel.setContrast(value); - }); - contrastSliderPanel.add(contrastLabel, BorderLayout.NORTH); - contrastSliderPanel.add(contrastSlider, BorderLayout.CENTER); - - brightnessPanel.add(brightnessSliderPanel); - brightnessPanel.add(Box.createRigidArea(new Dimension(0, 10))); - brightnessPanel.add(contrastSliderPanel); - - JButton resetButton = new JButton("Reset All Effects"); - resetButton.setAlignmentX(Component.CENTER_ALIGNMENT); - resetButton.addActionListener(e -> { - mirrorCheck.setSelected(false); - flipCheck.setSelected(false); - rotateCheck.setSelected(false); - grayscaleCheck.setSelected(false); - invertCheck.setSelected(false); - brightnessSlider.setValue(0); - contrastSlider.setValue(100); - cameraPanel.resetEffects(); - }); - - panel.add(transformPanel); - panel.add(Box.createRigidArea(new Dimension(0, 10))); - panel.add(colorPanel); - panel.add(Box.createRigidArea(new Dimension(0, 10))); - panel.add(brightnessPanel); - panel.add(Box.createRigidArea(new Dimension(0, 15))); - panel.add(resetButton); - panel.add(Box.createVerticalGlue()); - - return panel; - } - - private void browseDirectory(JTextField field, Component parent) { - JFileChooser chooser = new JFileChooser(field.getText()); - chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { - field.setText(chooser.getSelectedFile().getAbsolutePath()); - } - } - private void saveSnapshot(CameraPanel cameraPanel, Webcam webcam, String directory, Component parent) { BufferedImage img = cameraPanel.getCurrentProcessedImage(); if (img != null) { try { - File dir = new File(directory); - dir.mkdirs(); - - String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String filename = "screenshot_" + webcam.getName().replaceAll("[^a-zA-Z0-9]", "_") - + "_" + timestamp + ".png"; - File file = new File(dir, filename); + File file = new File(directory, "snap.png"); ImageIO.write(img, "PNG", file); } catch (Exception ex) { - JOptionPane.showMessageDialog(parent, - "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); } } } @@ -393,7 +220,69 @@ public class SwingIFrame { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { SwingIFrame dashboard = new SwingIFrame(); + // Example usage: dashboard.addCameraInternalFrame(Webcam.getDefault()); dashboard.show(); }); } + + // --- Inner Classes for Blender Styling --- + + static class BlenderDesktopPane extends JDesktopPane { + private final Map connections; + + public BlenderDesktopPane(Map connections) { + this.connections = connections; + } + + @Override + protected void paintChildren(Graphics g) { + super.paintChildren(g); + Graphics2D g2d = (Graphics2D) g.create(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + for (Map.Entry entry : connections.entrySet()) { + JInternalFrame camera = entry.getKey(); + EffectsPanelFrame effects = entry.getValue(); + + if (camera.isVisible() && effects.isVisible() && !camera.isIcon() && !effects.isIcon()) { + drawBezierConnection(g2d, camera, effects); + } + } + g2d.dispose(); + } + + private void drawBezierConnection(Graphics2D g2d, JInternalFrame from, JInternalFrame to) { + Rectangle f = from.getBounds(); + Rectangle t = to.getBounds(); + + int x1 = f.x + f.width; + int y1 = f.y + f.height / 2; + int x2 = t.x; + int y2 = t.y + t.height / 2; + + int ctrlOffset = Math.min(Math.abs(x2 - x1) / 2, 150); + CubicCurve2D curve = new CubicCurve2D.Double(x1, y1, x1 + ctrlOffset, y1, x2 - ctrlOffset, y2, x2, y2); + + g2d.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g2d.setColor(new Color(100, 200, 255, 200)); + g2d.draw(curve); + + g2d.fillOval(x1 - 5, y1 - 5, 10, 10); + g2d.fillOval(x2 - 5, y2 - 5, 10, 10); + } + } + + static class EffectsPanelFrame extends JInternalFrame { + public EffectsPanelFrame(String title, CameraPanel cameraPanel, JInternalFrame cameraFrame) { + super(title, true, true, true, true); + add(new FilterPanel(cameraPanel)); + + // Repaint curves when moving or iconifying + addComponentListener(new java.awt.event.ComponentAdapter() { + @Override public void componentMoved(java.awt.event.ComponentEvent e) { + if(getDesktopPane() != null) getDesktopPane().repaint(); + } + }); + } + } } \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/AutoGainProcessor.java b/src/main/java/io/swtc/proccessing/AutoGainProcessor.java index 7138dcd..473ba4c 100644 --- a/src/main/java/io/swtc/proccessing/AutoGainProcessor.java +++ b/src/main/java/io/swtc/proccessing/AutoGainProcessor.java @@ -1,12 +1,21 @@ package io.swtc.proccessing; -/* -* -* Soon to be deprecated! -* -* */ +/** + * Gray World Algorithm. + * + *

+ * This class is an implementation of the Gray World algorithm, an automatic + * white balancing method. + *

+ * + *

+ * See: + * + * Stanford explanation + * + *

+ */ -@Deprecated public class AutoGainProcessor { public float[] calculateAutoGains(int[] pixels) { diff --git a/src/main/java/io/swtc/proccessing/CameraPanel.java b/src/main/java/io/swtc/proccessing/CameraPanel.java index fda37a7..f0d5620 100644 --- a/src/main/java/io/swtc/proccessing/CameraPanel.java +++ b/src/main/java/io/swtc/proccessing/CameraPanel.java @@ -3,16 +3,16 @@ package io.swtc.proccessing; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; +import java.util.function.Function; -/* -* -* Now here its getting rather interesting! this class processes some -* important stuff! -* -* */ - +/** + * Enhanced CameraPanel with support for custom image processors + */ public class CameraPanel extends JPanel { private BufferedImage currentImage; + private BufferedImage processedImage; + + // Basic transform effects private boolean mirror = false; private boolean flip = false; private boolean rotate = false; @@ -21,150 +21,100 @@ public class CameraPanel extends JPanel { private int brightness = 0; private float contrast = 1.0f; + // Custom processor for advanced effects + private Function imageProcessor; + + public CameraPanel() { + setBackground(Color.BLACK); + setPreferredSize(new Dimension(640, 480)); + } + public void setImage(BufferedImage img) { this.currentImage = img; - this.repaint(); + processImage(); + repaint(); } - public BufferedImage getCurrentImage() { - return currentImage; + public void setImageProcessor(Function processor) { + this.imageProcessor = processor; + processImage(); + repaint(); } - public BufferedImage getCurrentProcessedImage() { + private void processImage() { if (currentImage == null) { - return null; + processedImage = null; + return; } - BufferedImage processed = currentImage; + BufferedImage result = currentImage; - // apply color effects - if (grayscale || invert || brightness != 0 || contrast != 1.0f) { - processed = applyColorEffects(processed); + // Apply basic transforms first + result = applyBasicEffects(result); + + // Apply custom processor if set + if (imageProcessor != null) { + result = imageProcessor.apply(result); } - // apply transform. - if (mirror || flip || rotate) { - processed = applyTransforms(processed); - } - - return processed; + processedImage = result; } - /* Helper Methods ... its the same boilerplate shit over and over again, im sick of this. */ - public void setMirror(boolean mirror) { - this.mirror = mirror; - this.repaint(); - } + private BufferedImage applyBasicEffects(BufferedImage img) { + int width = img.getWidth(); + int height = img.getHeight(); + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - public void setFlip(boolean flip) { - this.flip = flip; - this.repaint(); - } + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // Handle mirror/flip/rotate + int srcX = x; + int srcY = y; - public void setRotate(boolean rotate) { - this.rotate = rotate; - this.repaint(); - } + if (mirror) srcX = width - 1 - x; + if (flip) srcY = height - 1 - y; + if (rotate) { + int temp = srcX; + srcX = width - 1 - srcY; + srcY = temp; + } - public void setGrayscale(boolean grayscale) { - this.grayscale = grayscale; - this.repaint(); - } + // Ensure coordinates are in bounds + srcX = Math.max(0, Math.min(width - 1, srcX)); + srcY = Math.max(0, Math.min(height - 1, srcY)); - public void setInvert(boolean invert) { - this.invert = invert; - this.repaint(); - } - - public void setBrightness(int brightness) { - this.brightness = brightness; - this.repaint(); - } - - public void setContrast(float contrast) { - this.contrast = contrast; - this.repaint(); - } - - public void resetEffects() { - mirror = flip = rotate = grayscale = invert = false; - brightness = 0; - contrast = 1.0f; - this.repaint(); - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - if (currentImage != null) { - Graphics2D g2 = (Graphics2D) g; - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - - BufferedImage processedImage = currentImage; - - // effects - if (grayscale || invert || brightness != 0 || contrast != 1.0f) { - processedImage = applyColorEffects(processedImage); - } - - // transforms - int w = getWidth(), h = getHeight(); - - if (rotate) { - g2.translate(w / 2, h / 2); - g2.rotate(Math.PI); - g2.translate(-w / 2, -h / 2); - } - - - // here we have if, cause we need to do the stuff, yk? - if (mirror && flip) { - g2.drawImage(processedImage, w, h, -w, -h, null); - } else if (mirror) { - g2.drawImage(processedImage, w, 0, -w, h, null); - } else if (flip) { - g2.drawImage(processedImage, 0, h, w, -h, null); - } else { - g2.drawImage(processedImage, 0, 0, w, h, null); - } - } - } - - private BufferedImage applyColorEffects(BufferedImage img) { - BufferedImage result = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB); - - for (int y = 0; y < img.getHeight(); y++) { - for (int x = 0; x < img.getWidth(); x++) { - int rgb = img.getRGB(x, y); + int rgb = img.getRGB(srcX, srcY); int r = (rgb >> 16) & 0xFF; int g = (rgb >> 8) & 0xFF; int b = rgb & 0xFF; + // Apply grayscale if (grayscale) { int gray = (r + g + b) / 3; r = g = b = gray; } - // this is fun, this regulates the brightness or the contrast! - // this is some real java, the other stuff is just UI design, which i am bad at, - // but this! This is some real shit - r = (int) ((r - 128) * contrast + 128 + brightness); - g = (int) ((g - 128) * contrast + 128 + brightness); - b = (int) ((b - 128) * contrast + 128 + brightness); + // Apply brightness + if (brightness != 0) { + r = clamp(r + brightness); + g = clamp(g + brightness); + b = clamp(b + brightness); + } - // invert the colors! + // Apply contrast + if (contrast != 1.0f) { + r = clamp((int)((r - 128) * contrast + 128)); + g = clamp((int)((g - 128) * contrast + 128)); + b = clamp((int)((b - 128) * contrast + 128)); + } + + // Apply invert if (invert) { r = 255 - r; g = 255 - g; b = 255 - b; } - // clamp so we dont get into weird color grades, or any weird looking spaces - // if we dont do this we cant really do stuff which looks bearable - r = Math.max(0, Math.min(255, r)); - g = Math.max(0, Math.min(255, g)); - b = Math.max(0, Math.min(255, b)); - result.setRGB(x, y, (r << 16) | (g << 8) | b); } } @@ -172,33 +122,95 @@ public class CameraPanel extends JPanel { return result; } - private BufferedImage applyTransforms(BufferedImage img) { - int width = img.getWidth(); - int height = img.getHeight(); - BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + private int clamp(int value) { + return Math.max(0, Math.min(255, value)); + } - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int sourceX = x; - int sourceY = y; + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); - if (mirror) { - sourceX = width - 1 - x; - } - if (flip) { - sourceY = height - 1 - y; - } - if (rotate) { - int tempX = width - 1 - sourceX; - int tempY = height - 1 - sourceY; - sourceX = tempX; - sourceY = tempY; - } + if (processedImage != null) { + Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - result.setRGB(x, y, img.getRGB(sourceX, sourceY)); - } + // Scale image to fit panel while maintaining aspect ratio + int panelWidth = getWidth(); + int panelHeight = getHeight(); + int imgWidth = processedImage.getWidth(); + int imgHeight = processedImage.getHeight(); + + double scaleX = (double) panelWidth / imgWidth; + double scaleY = (double) panelHeight / imgHeight; + double scale = Math.min(scaleX, scaleY); + + int scaledWidth = (int) (imgWidth * scale); + int scaledHeight = (int) (imgHeight * scale); + + int x = (panelWidth - scaledWidth) / 2; + int y = (panelHeight - scaledHeight) / 2; + + g2d.drawImage(processedImage, x, y, scaledWidth, scaledHeight, null); } + } - return result; + public BufferedImage getCurrentProcessedImage() { + return processedImage; + } + + // Basic effect setters + public void setMirror(boolean mirror) { + this.mirror = mirror; + processImage(); + repaint(); + } + + public void setFlip(boolean flip) { + this.flip = flip; + processImage(); + repaint(); + } + + public void setRotate(boolean rotate) { + this.rotate = rotate; + processImage(); + repaint(); + } + + public void setGrayscale(boolean grayscale) { + this.grayscale = grayscale; + processImage(); + repaint(); + } + + public void setInvert(boolean invert) { + this.invert = invert; + processImage(); + repaint(); + } + + public void setBrightness(int brightness) { + this.brightness = brightness; + processImage(); + repaint(); + } + + public void setContrast(float contrast) { + this.contrast = contrast; + processImage(); + repaint(); + } + + public void resetEffects() { + this.mirror = false; + this.flip = false; + this.rotate = false; + this.grayscale = false; + this.invert = false; + this.brightness = 0; + this.contrast = 1.0f; + this.imageProcessor = null; + processImage(); + repaint(); } } \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ConnectionOverlay.java b/src/main/java/io/swtc/proccessing/ConnectionOverlay.java new file mode 100644 index 0000000..9be93fc --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ConnectionOverlay.java @@ -0,0 +1,81 @@ +package io.swtc.proccessing; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.CubicCurve2D; + +/** + * Acts as a transparent layer over the entire window to draw connections. + */ +public class ConnectionOverlay extends JComponent { + private final Component source; + private final Component target; + private final Color connectionColor; + + public ConnectionOverlay(Component source, Component target, Color color) { + this.source = source; + this.target = target; + this.connectionColor = color; + setOpaque(false); // Make sure we can see through it + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (!source.isShowing() || !target.isShowing()) return; + + Graphics2D g2d = (Graphics2D) g.create(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 1. Get absolute positions relative to this Overlay + Point p1 = SwingUtilities.convertPoint(source, 0, 0, this); + Point p2 = SwingUtilities.convertPoint(target, 0, 0, this); + + int x1, y1, x2, y2, ctrl1X, ctrl2X; + + // Calculate vertical centers + y1 = p1.y + (source.getHeight() / 2); + y2 = p2.y + (target.getHeight() / 2); + + // 2. Logic to determine Left/Right orientation + // If source is to the left of target + if (p1.x + source.getWidth() < p2.x) { + x1 = p1.x + source.getWidth(); // Right edge of source + x2 = p2.x; // Left edge of target + } + // If source is to the right of target + else if (p1.x > p2.x + target.getWidth()) { + x1 = p1.x; // Left edge of source + x2 = p2.x + target.getWidth(); // Right edge of target + } + // If they are overlapping horizontally, use centers + else { + x1 = p1.x + (source.getWidth() / 2); + x2 = p2.x + (target.getWidth() / 2); + } + + // 3. Dynamic Curve "Stiffness" + // The horizontal distance between the two points determines how far the curve pulls + int horizontalDist = Math.abs(x1 - x2); + int offset = Math.max(horizontalDist / 2, 20); // Minimum 20px pull for short distances + + if (x1 < x2) { + ctrl1X = x1 + offset; + ctrl2X = x2 - offset; + } else { + ctrl1X = x1 - offset; + ctrl2X = x2 + offset; + } + + // 4. Draw the Curve + g2d.setColor(connectionColor); + g2d.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + + // We keep the Y coordinates for controls the same as the endpoints + // to create that "horizontal entry/exit" look. + CubicCurve2D curve = new CubicCurve2D.Float(x1, y1, ctrl1X, y1, ctrl2X, y2, x2, y2); + g2d.draw(curve); + + g2d.dispose(); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/EffectState.java b/src/main/java/io/swtc/proccessing/EffectState.java new file mode 100644 index 0000000..c0d45ce --- /dev/null +++ b/src/main/java/io/swtc/proccessing/EffectState.java @@ -0,0 +1,25 @@ +package io.swtc.proccessing; + +public class EffectState { + public final boolean awbEnabled, dnrEnabled, edgeEnhance; + public final int awbStrength, dnrSpatial, dnrTemporal; + public final int temperature, tint, saturation, shadows, highlights, sharpness; + + public EffectState(boolean awbEnabled, int awbStrength, boolean dnrEnabled, + int dnrSpatial, int dnrTemporal, int temperature, int tint, + int saturation, int shadows, int highlights, + int sharpness, boolean edgeEnhance) { + this.awbEnabled = awbEnabled; + this.awbStrength = awbStrength; + this.dnrEnabled = dnrEnabled; + this.dnrSpatial = dnrSpatial; + this.dnrTemporal = dnrTemporal; + this.temperature = temperature; + this.tint = tint; + this.saturation = saturation; + this.shadows = shadows; + this.highlights = highlights; + this.sharpness = sharpness; + this.edgeEnhance = edgeEnhance; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/FilterPanel.java b/src/main/java/io/swtc/proccessing/FilterPanel.java new file mode 100644 index 0000000..2258e42 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/FilterPanel.java @@ -0,0 +1,273 @@ +package io.swtc.proccessing; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; +import java.awt.*; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.image.BufferedImage; +import java.util.Random; + +public class FilterPanel extends JPanel { + + private final CameraPanel cameraPanel; + private final AutoGainProcessor autoGainProcessor; + private final PresetLibrary presetLibrary; + private final Color connectionColor; + + private JCheckBox awbEnabled, dnrEnabled, edgeEnhance; + private JSlider awbStrength, dnrStrength, dnrTemporal, sharpness; + private JSlider saturation, temperature, tint, shadows, highlights; + private JComboBox presetCombo; + + private float[] currentGains = {1f, 1f, 1f}; + + public FilterPanel(CameraPanel cameraPanel) { + this.cameraPanel = cameraPanel; + this.autoGainProcessor = new AutoGainProcessor(); + this.presetLibrary = new PresetLibrary(); + + // FIX: Randomize color with high saturation so it stands out + Random rand = new Random(); + this.connectionColor = Color.getHSBColor(rand.nextFloat(), 0.9f, 0.9f); + + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + setBorder(new EmptyBorder(10, 10, 10, 10)); + + buildUI(); + initListeners(); // Initialize anti-ghosting listeners + } + + + private void buildUI() { + add(createPresetPanel()); + add(Box.createRigidArea(new Dimension(0, 10))); + + JPanel container = new JPanel(); + container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS)); + + container.add(createAWBPanel()); + container.add(Box.createRigidArea(new Dimension(0, 10))); + container.add(createDNRPanel()); + container.add(Box.createRigidArea(new Dimension(0, 10))); + container.add(createColorPanel()); + container.add(Box.createRigidArea(new Dimension(0, 10))); + container.add(createDetailPanel()); + + JScrollPane scroll = new JScrollPane(container); + scroll.setBorder(null); + scroll.getVerticalScrollBar().setUnitIncrement(16); + add(scroll); + + add(Box.createRigidArea(new Dimension(0, 10))); + add(createActionPanel()); + } + + public EffectState getCurrentState() { + return new EffectState( + awbEnabled.isSelected(), awbStrength.getValue(), + dnrEnabled.isSelected(), dnrStrength.getValue(), dnrTemporal.getValue(), + temperature.getValue(), tint.getValue(), saturation.getValue(), + shadows.getValue(), highlights.getValue(), + sharpness.getValue(), edgeEnhance.isSelected() + ); + } + + // Inside AdvancedEffectsPanel.java + + private void initListeners() { + ComponentAdapter repaintListener = new ComponentAdapter() { + @Override + public void componentMoved(ComponentEvent e) { + updateOverlay(); + } + @Override + public void componentResized(ComponentEvent e) { + updateOverlay(); + } + }; + + this.addComponentListener(repaintListener); + if (this.cameraPanel != null) { + this.cameraPanel.addComponentListener(repaintListener); + } + } + + private void updateOverlay() { + RootPaneContainer root = (RootPaneContainer) SwingUtilities.getWindowAncestor(this); + if (root != null) { + root.getGlassPane().repaint(); + } + } + + private void applyToCamera() { + EffectState state = getCurrentState(); + cameraPanel.setImageProcessor(img -> + ImageEffectEngine.applyEffects(img, state, currentGains) + ); + } + + private JPanel createPresetPanel() { + JPanel panel = new JPanel(new BorderLayout(5, 5)); + panel.setBorder(createTitledBorder("Presets")); + + presetCombo = new JComboBox<>(new String[]{"Custom", "Natural", "Vivid", "Portrait", "Low Light", "Cinematic"}); + JButton saveBtn = new JButton("Save"); + + presetCombo.addActionListener(e -> { + String selected = (String) presetCombo.getSelectedItem(); + EffectState state = presetLibrary.get(selected); + if (state != null) applyPresetToUI(state); + }); + + saveBtn.addActionListener(e -> { + String name = JOptionPane.showInputDialog(this, "Preset Name:"); + if (name != null) { + presetLibrary.savePreset(name, getCurrentState()); + presetCombo.addItem(name); + } + }); + + panel.add(presetCombo, BorderLayout.CENTER); + panel.add(saveBtn, BorderLayout.EAST); + return panel; + } + + private JPanel createDNRPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(createTitledBorder("3D Denoise (DNR)")); + + dnrEnabled = new JCheckBox("Enable Temporal Denoise"); + dnrStrength = addSliderToPanel(panel, "Spatial Strength", 0, 100, 30, "%"); + dnrTemporal = addSliderToPanel(panel, "Temporal Strength", 0, 100, 50, "%"); + + dnrEnabled.addActionListener(e -> { + boolean enabled = dnrEnabled.isSelected(); + dnrStrength.setEnabled(enabled); + dnrTemporal.setEnabled(enabled); + applyToCamera(); + }); + + dnrStrength.setEnabled(false); + dnrTemporal.setEnabled(false); + + panel.add(dnrEnabled, 0); + return panel; + } + + private JPanel createDetailPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(createTitledBorder("Detail & Sharpness")); + + sharpness = addSliderToPanel(panel, "Sharpness", 0, 200, 100, "%"); + edgeEnhance = new JCheckBox("Edge Enhancement"); + + edgeEnhance.addActionListener(e -> applyToCamera()); + + panel.add(edgeEnhance); + return panel; + } + + private JPanel createAWBPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(createTitledBorder("White Balance")); + + awbEnabled = new JCheckBox("Enable AWB"); + JPanel sPanel = createSliderPanel("Strength", 0, 100, 100, "%"); + awbStrength = (JSlider) sPanel.getComponent(1); + JButton balanceBtn = new JButton("Balance Now"); + + awbEnabled.addActionListener(e -> { + boolean active = awbEnabled.isSelected(); + awbStrength.setEnabled(active); + balanceBtn.setEnabled(active); + if (active) performOneTimeBalance(); + else { currentGains = new float[]{1f, 1f, 1f}; applyToCamera(); } + }); + + awbStrength.addChangeListener(e -> { if(!awbStrength.getValueIsAdjusting()) applyToCamera(); }); + balanceBtn.addActionListener(e -> performOneTimeBalance()); + + panel.add(awbEnabled); + panel.add(sPanel); + panel.add(balanceBtn); + return panel; + } + + private JPanel createColorPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(createTitledBorder("Color Grading")); + + temperature = addSliderToPanel(panel, "Temperature", -100, 100, 0, ""); + tint = addSliderToPanel(panel, "Tint", -100, 100, 0, ""); + saturation = addSliderToPanel(panel, "Saturation", 0, 200, 100, "%"); + shadows = addSliderToPanel(panel, "Shadows", -100, 100, 0, ""); + highlights = addSliderToPanel(panel, "Highlights", -100, 100, 0, ""); + + return panel; + } + + private void performOneTimeBalance() { + BufferedImage img = cameraPanel.getCurrentProcessedImage(); + if (img != null) { + int[] pixels = img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth()); + currentGains = autoGainProcessor.calculateAutoGains(pixels); + applyToCamera(); + } + } + + private void applyPresetToUI(EffectState s) { + awbEnabled.setSelected(s.awbEnabled); + awbStrength.setValue(s.awbStrength); + dnrEnabled.setSelected(s.dnrEnabled); + dnrStrength.setValue(s.dnrSpatial); + temperature.setValue(s.temperature); + tint.setValue(s.tint); + saturation.setValue(s.saturation); + shadows.setValue(s.shadows); + highlights.setValue(s.highlights); + sharpness.setValue(s.sharpness); + edgeEnhance.setSelected(s.edgeEnhance); + applyToCamera(); + } + + private JSlider addSliderToPanel(JPanel parent, String label, int min, int max, int val, String unit) { + JPanel p = createSliderPanel(label, min, max, val, unit); + JSlider s = (JSlider) p.getComponent(1); + s.addChangeListener(e -> { if(!s.getValueIsAdjusting()) applyToCamera(); }); + parent.add(p); + parent.add(Box.createRigidArea(new Dimension(0, 5))); + return s; + } + + private JPanel createSliderPanel(String label, int min, int max, int initial, String unit) { + JPanel panel = new JPanel(new BorderLayout()); + JLabel title = new JLabel(label + ": " + initial + unit); + JSlider slider = new JSlider(min, max, initial); + slider.addChangeListener(e -> title.setText(label + ": " + slider.getValue() + unit)); + panel.add(title, BorderLayout.NORTH); + panel.add(slider, BorderLayout.CENTER); + return panel; + } + + private TitledBorder createTitledBorder(String title) { + return BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), title); + } + + private JPanel createActionPanel() { + JPanel panel = new JPanel(new GridLayout(1, 2, 10, 0)); + JButton reset = new JButton("Reset All"); + reset.addActionListener(e -> resetUI()); + panel.add(reset); + return panel; + } + + private void resetUI() { + applyPresetToUI(new EffectState(false, 100, false, 30, 50, 0, 0, 100, 0, 0, 100, false)); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ImageEffectEngine.java b/src/main/java/io/swtc/proccessing/ImageEffectEngine.java new file mode 100644 index 0000000..65eb22f --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ImageEffectEngine.java @@ -0,0 +1,147 @@ +package io.swtc.proccessing; + +import java.awt.image.BufferedImage; + +public class ImageEffectEngine { + + public static BufferedImage applyEffects(BufferedImage img, EffectState state, float[] currentGains) { + if (img == null) return null; + + int width = img.getWidth(); + int height = img.getHeight(); + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rgb = img.getRGB(x, y); + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + + // 1. AWB + if (state.awbEnabled) { + float s = state.awbStrength / 100f; + r = (int) Math.min(255, r * (1 + (currentGains[0] - 1) * s)); + g = (int) Math.min(255, g * (1 + (currentGains[1] - 1) * s)); + b = (int) Math.min(255, b * (1 + (currentGains[2] - 1) * s)); + } + + // 2. Temp & Tint + if (state.temperature != 0) { + float factor = state.temperature / 100f; + r = clamp(r + (int)(factor * 30)); + b = clamp(b - (int)(factor * 30)); + } + if (state.tint != 0) { + g = clamp(g + (int)((state.tint / 100f) * 20)); + } + + // 3. Saturation + if (state.saturation != 100) { + float factor = state.saturation / 100f; + float gray = (r + g + b) / 3f; + r = clamp((int)(gray + (r - gray) * factor)); + g = clamp((int)(gray + (g - gray) * factor)); + b = clamp((int)(gray + (b - gray) * factor)); + } + + // 4. Shadows/Highlights + float lum = (r + g + b) / 3f / 255f; + if (lum < 0.5f && state.shadows != 0) { + int adj = (int)((state.shadows / 100f) * (1 - lum * 2) * 50); + r = clamp(r + adj); g = clamp(g + adj); b = clamp(b + adj); + } else if (lum > 0.5f && state.highlights != 0) { + int adj = (int)((state.highlights / 100f) * (lum * 2 - 1) * 50); + r = clamp(r + adj); g = clamp(g + adj); b = clamp(b + adj); + } + + result.setRGB(x, y, (r << 16) | (g << 8) | b); + } + } + + if (state.sharpness != 100 || state.edgeEnhance) { + result = applySharpness(result, state.sharpness / 100f, state.edgeEnhance); + } + + if (state.dnrEnabled) { + result = applyDenoise(result, state.dnrSpatial / 100f); + } + + return result; + } + + private static BufferedImage applySharpness(BufferedImage img, float amount, boolean edgeEnhance) { + if (amount == 1f && !edgeEnhance) return img; + + int width = img.getWidth(); + int height = img.getHeight(); + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + float[][] kernel = edgeEnhance ? + new float[][]{{0, -1, 0}, {-1, 5, -1}, {0, -1, 0}} : + new float[][]{{-1, -1, -1}, {-1, 9, -1}, {-1, -1, -1}}; + + for (int y = 1; y < height - 1; y++) { + for (int x = 1; x < width - 1; x++) { + float r = 0, g = 0, b = 0; + + for (int ky = -1; ky <= 1; ky++) { + for (int kx = -1; kx <= 1; kx++) { + int rgb = img.getRGB(x + kx, y + ky); + float weight = kernel[ky + 1][kx + 1] * (amount - 1) / 8f; + if (kx == 0 && ky == 0) weight += 1; + + r += ((rgb >> 16) & 0xFF) * weight; + g += ((rgb >> 8) & 0xFF) * weight; + b += (rgb & 0xFF) * weight; + } + } + + result.setRGB(x, y, (clamp((int)r) << 16) | (clamp((int)g) << 8) | clamp((int)b)); + } + } + + return result; + } + + private static BufferedImage applyDenoise(BufferedImage img, float strength) { + if (strength == 0) return img; + + int width = img.getWidth(); + int height = img.getHeight(); + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + int radius = (int)(strength * 2) + 1; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rSum = 0, gSum = 0, bSum = 0, count = 0; + + for (int dy = -radius; dy <= radius; dy++) { + for (int dx = -radius; dx <= radius; dx++) { + int nx = Math.min(width - 1, Math.max(0, x + dx)); + int ny = Math.min(height - 1, Math.max(0, y + dy)); + + int rgb = img.getRGB(nx, ny); + rSum += (rgb >> 16) & 0xFF; + gSum += (rgb >> 8) & 0xFF; + bSum += rgb & 0xFF; + count++; + } + } + + int r = rSum / count; + int g = gSum / count; + int b = bSum / count; + + result.setRGB(x, y, (r << 16) | (g << 8) | b); + } + } + + return result; + } + + private static int clamp(int val) { + return Math.max(0, Math.min(255, val)); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/PresetLibrary.java b/src/main/java/io/swtc/proccessing/PresetLibrary.java new file mode 100644 index 0000000..56f81ca --- /dev/null +++ b/src/main/java/io/swtc/proccessing/PresetLibrary.java @@ -0,0 +1,46 @@ +package io.swtc.proccessing; + +import java.util.HashMap; +import java.util.Map; + +public class PresetLibrary { + private final Map presets = new HashMap<>(); + + public PresetLibrary() { + presets.put("Natural", new EffectState( + false, 100, false, 20, 40, 0, 0, 100, 0, 0, 100, false + )); + + presets.put("Vivid", new EffectState( + true, 100, false, 15, 30, 10, 5, 130, 0, 0, 120, true + )); + + presets.put("Portrait", new EffectState( + true, 80, true, 40, 50, -5, 10, 95, 10, -5, 90, false + )); + + presets.put("Low Light", new EffectState( + true, 100, true, 60, 70, 0, 0, 110, 20, -10, 80, false + )); + + presets.put("High Contrast", new EffectState( + false, 100, false, 25, 35, 0, 0, 120, -20, 20, 130, true + )); + + presets.put("Cinematic", new EffectState( + true, 70, true, 30, 45, -15, -5, 90, -10, 5, 110, false + )); + } + + public void savePreset(String name, EffectState state) { + presets.put(name, state); + } + + public EffectState get(String name) { + return presets.get(name); + } + + public String[] getPresetNames() { + return presets.keySet().toArray(new String[0]); + } +} \ No newline at end of file