Spline Editor
/** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import java.awt.*; import java.awt.datatransfer.*; import java.awt.event.*; import java.awt.geom.*; import java.beans.*; import java.io.*; import java.text.*; import java.util.*; import javax.imageio.*; import javax.swing.*; import javax.swing.event.*; import java.awt.image.*; import org.jdesktop.animation.timing.interpolation.*; import org.jdesktop.animation.timing.*; import org.jdesktop.animation.timing.Animator.*; import org.jdesktop.animation.timing.interpolation.*; import java.awt.image.*; import javax.swing.border.*; import java.net.*; public class SplineEditor extends JFrame { public SplineEditor() throws HeadlessException { super("Spline Editor"); add(buildHeader(), BorderLayout.NORTH); add(buildControlPanel(), BorderLayout.CENTER); pack(); setLocationRelativeTo(null); setResizable(false); setDefaultCloseOperation(EXIT_ON_CLOSE); } private Component buildHeader() { ImageIcon icon = new ImageIcon(getClass().getResource("simulator.png")); HeaderPanel header = new HeaderPanel(icon, "Timing Framework Spline Editor", "Drag control points in the display to change the shape of the spline.", "Click the Copy Code button to generate the corresponding Java code."); return header; } private Component buildControlPanel() { return new SplineControlPanel(); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException e) { } catch (InstantiationException e) { } catch (IllegalAccessException e) { } catch (UnsupportedLookAndFeelException e) { } new SplineEditor().setVisible(true); } }); } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class EquationDisplay extends JComponent implements PropertyChangeListener { private static final Color COLOR_BACKGROUND = Color.WHITE; private static final Color COLOR_MAJOR_GRID = Color.GRAY.brighter(); private static final Color COLOR_MINOR_GRID = new Color(220, 220, 220); private static final Color COLOR_AXIS = Color.BLACK; private static final float STROKE_AXIS = 1.2f; private static final float STROKE_GRID = 1.0f; private static final float COEFF_ZOOM = 1.1f; private java.util.List<DrawableEquation> equations; protected double minX; protected double maxX; protected double minY; protected double maxY; private double originX; private double originY; private double majorX; private int minorX; private double majorY; private int minorY; private boolean drawText = true; private Point dragStart; private NumberFormat formatter; private ZoomHandler zoomHandler; private PanMotionHandler panMotionHandler; private PanHandler panHandler; public EquationDisplay(double originX, double originY, double minX, double maxX, double minY, double maxY, double majorX, int minorX, double majorY, int minorY) { if (minX >= maxX) { throw new IllegalArgumentException("minX must be < to maxX"); } if (originX < minX || originX > maxX) { throw new IllegalArgumentException("originX must be between minX and maxX"); } if (minY >= maxY) { throw new IllegalArgumentException("minY must be < to maxY"); } if (originY < minY || originY > maxY) { throw new IllegalArgumentException("originY must be between minY and maxY"); } if (minorX <= 0) { throw new IllegalArgumentException("minorX must be > 0"); } if (minorY <= 0) { throw new IllegalArgumentException("minorY must be > 0"); } if (majorX <= 0.0) { throw new IllegalArgumentException("majorX must be > 0.0"); } if (majorY <= 0.0) { throw new IllegalArgumentException("majorY must be > 0.0"); } this.originX = originX; this.originY = originY; this.minX = minX; this.maxX = maxX; this.minY = minY; this.maxY = maxY; this.majorX = majorX; this.minorX = minorX; this.majorY = majorY; this.minorY = minorY; this.equations = new LinkedList<DrawableEquation>(); this.formatter = NumberFormat.getInstance(); this.formatter.setMaximumFractionDigits(2); panHandler = new PanHandler(); addMouseListener(panHandler); panMotionHandler = new PanMotionHandler(); addMouseMotionListener(panMotionHandler); zoomHandler = new ZoomHandler(); addMouseWheelListener(zoomHandler); } @Override public void setEnabled(boolean enabled) { if (isEnabled() != enabled) { //super.setEnabled(enabled); if (enabled) { addMouseListener(panHandler); addMouseMotionListener(panMotionHandler); addMouseWheelListener(zoomHandler); } else { removeMouseListener(panHandler); removeMouseMotionListener(panMotionHandler); removeMouseWheelListener(zoomHandler); } } } public boolean isDrawText() { return drawText; } public void setDrawText(boolean drawText) { this.drawText = drawText; } public void addEquation(AbstractEquation equation, Color color) { if (equation != null && !equations.contains(equation)) { equation.addPropertyChangeListener(this); equations.add(new DrawableEquation(equation, color)); repaint(); } } public void removeEquation(AbstractEquation equation) { if (equation != null) { DrawableEquation toRemove = null; for (DrawableEquation drawable: equations) { if (drawable.getEquation() == equation) { toRemove = drawable; break; } } if (toRemove != null) { equation.removePropertyChangeListener(this); equations.remove(toRemove); repaint(); } } } @Override public Dimension getPreferredSize() { return new Dimension(400, 400); } public void propertyChange(PropertyChangeEvent evt) { repaint(); } protected double yPositionToPixel(double position) { double height = (double) getHeight(); return height - ((position - minY) * height / (maxY - minY)); } protected double xPositionToPixel(double position) { return (position - minX) * (double) getWidth() / (maxX - minX); } protected double xPixelToPosition(double pixel) { double axisV = xPositionToPixel(originX); return (pixel - axisV) * (maxX - minX) / (double) getWidth(); } protected double yPixelToPosition(double pixel) { double axisH = yPositionToPixel(originY); return (getHeight() - pixel - axisH) * (maxY - minY) / (double) getHeight(); } @Override protected void paintComponent(Graphics g) { if (!isVisible()) { return; } Graphics2D g2 = (Graphics2D) g; setupGraphics(g2); paintBackground(g2); drawGrid(g2); drawAxis(g2); drawEquations(g2); paintInformation(g2); } protected void paintInformation(Graphics2D g2) { } private void drawEquations(Graphics2D g2) { for (DrawableEquation drawable: equations) { g2.setColor(drawable.getColor()); drawEquation(g2, drawable.getEquation()); } } private void drawEquation(Graphics2D g2, AbstractEquation equation) { float x = 0.0f; float y = (float) yPositionToPixel(equation.compute(xPixelToPosition(0.0))); GeneralPath path = new GeneralPath(); path.moveTo(x, y); for (x = 0.0f; x < getWidth(); x += 1.0f) { double position = xPixelToPosition(x); y = (float) yPositionToPixel(equation.compute(position)); path.lineTo(x, y); } g2.draw(path); } private void drawGrid(Graphics2D g2) { Stroke stroke = g2.getStroke(); drawVerticalGrid(g2); drawHorizontalGrid(g2); if (drawText) { drawVerticalLabels(g2); drawHorizontalLabels(g2); } g2.setStroke(stroke); } private void drawHorizontalLabels(Graphics2D g2) { double axisV = xPositionToPixel(originX); g2.setColor(COLOR_AXIS); for (double y = originY + majorY; y < maxY + majorY; y += majorY) { int position = (int) yPositionToPixel(y); g2.drawString(formatter.format(y), (int) axisV + 5, position); } for (double y = originY - majorY; y > minY - majorY; y -= majorY) { int position = (int) yPositionToPixel(y); g2.drawString(formatter.format(y), (int) axisV + 5, position); } } private void drawHorizontalGrid(Graphics2D g2) { double minorSpacing = majorY / minorY; double axisV = xPositionToPixel(originX); Stroke gridStroke = new BasicStroke(STROKE_GRID); Stroke axisStroke = new BasicStroke(STROKE_AXIS); for (double y = originY + majorY; y < maxY + majorY; y += majorY) { g2.setStroke(gridStroke); g2.setColor(COLOR_MINOR_GRID); for (int i = 0; i < minorY; i++) { int position = (int) yPositionToPixel(y - i * minorSpacing); g2.drawLine(0, position, getWidth(), position); } int position = (int) yPositionToPixel(y); g2.setColor(COLOR_MAJOR_GRID); g2.drawLine(0, position, getWidth(), position); g2.setStroke(axisStroke); g2.setColor(COLOR_AXIS); g2.drawLine((int) axisV - 3, position, (int) axisV + 3, position); } for (double y = originY - majorY; y > minY - majorY; y -= majorY) { g2.setStroke(gridStroke); g2.setColor(COLOR_MINOR_GRID); for (int i = 0; i < minorY; i++) { int position = (int) yPositionToPixel(y + i * minorSpacing); g2.drawLine(0, position, getWidth(), position); } int position = (int) yPositionToPixel(y); g2.setColor(COLOR_MAJOR_GRID); g2.drawLine(0, position, getWidth(), position); g2.setStroke(axisStroke); g2.setColor(COLOR_AXIS); g2.drawLine((int) axisV - 3, position, (int) axisV + 3, position); } } private void drawVerticalLabels(Graphics2D g2) { double axisH = yPositionToPixel(originY); FontMetrics metrics = g2.getFontMetrics(); g2.setColor(COLOR_AXIS); for (double x = originX + majorX; x < maxX + majorX; x += majorX) { int position = (int) xPositionToPixel(x); g2.drawString(formatter.format(x), position, (int) axisH + metrics.getHeight()); } for (double x = originX - majorX; x > minX - majorX; x -= majorX) { int position = (int) xPositionToPixel(x); g2.drawString(formatter.format(x), position, (int) axisH + metrics.getHeight()); } } private void drawVerticalGrid(Graphics2D g2) { double minorSpacing = majorX / minorX; double axisH = yPositionToPixel(originY); Stroke gridStroke = new BasicStroke(STROKE_GRID); Stroke axisStroke = new BasicStroke(STROKE_AXIS); for (double x = originX + majorX; x < maxX + majorX; x += majorX) { g2.setStroke(gridStroke); g2.setColor(COLOR_MINOR_GRID); for (int i = 0; i < minorX; i++) { int position = (int) xPositionToPixel(x - i * minorSpacing); g2.drawLine(position, 0, position, getHeight()); } int position = (int) xPositionToPixel(x); g2.setColor(COLOR_MAJOR_GRID); g2.drawLine(position, 0, position, getHeight()); g2.setStroke(axisStroke); g2.setColor(COLOR_AXIS); g2.drawLine(position, (int) axisH - 3, position, (int) axisH + 3); } for (double x = originX - majorX; x > minX - majorX; x -= majorX) { g2.setStroke(gridStroke); g2.setColor(COLOR_MINOR_GRID); for (int i = 0; i < minorX; i++) { int position = (int) xPositionToPixel(x + i * minorSpacing); g2.drawLine(position, 0, position, getHeight()); } int position = (int) xPositionToPixel(x); g2.setColor(COLOR_MAJOR_GRID); g2.drawLine(position, 0, position, getHeight()); g2.setStroke(axisStroke); g2.setColor(COLOR_AXIS); g2.drawLine(position, (int) axisH - 3, position, (int) axisH + 3); } } private void drawAxis(Graphics2D g2) { double axisH = yPositionToPixel(originY); double axisV = xPositionToPixel(originX); g2.setColor(COLOR_AXIS); Stroke stroke = g2.getStroke(); g2.setStroke(new BasicStroke(STROKE_AXIS)); g2.drawLine(0, (int) axisH, getWidth(), (int) axisH); g2.drawLine((int) axisV, 0, (int) axisV, getHeight()); FontMetrics metrics = g2.getFontMetrics(); g2.drawString(formatter.format(0.0), (int) axisV + 5, (int) axisH + metrics.getHeight()); g2.setStroke(stroke); } protected void setupGraphics(Graphics2D g2) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } protected void paintBackground(Graphics2D g2) { g2.setColor(COLOR_BACKGROUND); g2.fill(g2.getClipBounds()); } private class DrawableEquation { private AbstractEquation equation; private Color color; DrawableEquation(AbstractEquation equation, Color color) { this.equation = equation; this.color = color; } AbstractEquation getEquation() { return equation; } Color getColor() { return color; } } private class ZoomHandler implements MouseWheelListener { public void mouseWheelMoved(MouseWheelEvent e) { double distanceX = maxX - minX; double distanceY = maxY - minY; double cursorX = minX + distanceX / 2.0; double cursorY = minY + distanceY / 2.0; int rotation = e.getWheelRotation(); if (rotation < 0) { distanceX /= COEFF_ZOOM; distanceY /= COEFF_ZOOM; } else { distanceX *= COEFF_ZOOM; distanceY *= COEFF_ZOOM; } minX = cursorX - distanceX / 2.0; maxX = cursorX + distanceX / 2.0; minY = cursorY - distanceY / 2.0; maxY = cursorY + distanceY / 2.0; repaint(); } } private class PanHandler extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { dragStart = e.getPoint(); } } private class PanMotionHandler extends MouseMotionAdapter { @Override public void mouseDragged(MouseEvent e) { Point dragEnd = e.getPoint(); double distance = xPixelToPosition(dragEnd.getX()) - xPixelToPosition(dragStart.getX()); minX -= distance; maxX -= distance; distance = yPixelToPosition(dragEnd.getY()) - yPixelToPosition(dragStart.getY()); minY -= distance; maxY -= distance; repaint(); dragStart = dragEnd; } } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ interface Equation { public double compute(double variable); } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ abstract class AbstractEquation implements Equation { protected java.util.List<PropertyChangeListener> listeners; protected AbstractEquation() { this.listeners = new LinkedList<PropertyChangeListener>(); } public void addPropertyChangeListener(PropertyChangeListener listener) { if (listener != null && !listeners.contains(listener)) { listeners.add(listener); } } public void removePropertyChangeListener(PropertyChangeListener listener) { if (listener != null) { listeners.remove(listener); } } protected void firePropertyChange(String propertyName, double oldValue, double newValue) { PropertyChangeEvent changeEvent = new PropertyChangeEvent(this, propertyName, oldValue, newValue); for (PropertyChangeListener listener: listeners) { listener.propertyChange(changeEvent); } } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class AbstractSimulator extends JComponent { protected double time; public AbstractSimulator() { this.time = 0.0f; } public void setTime(double time) { this.time = time; repaint(); } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class BouncerSimulator extends AbstractSimulator { private static final Color COLOR_BACKGROUND = Color.WHITE; private BufferedImage image; public BouncerSimulator() { try { image = ImageIO.read(BouncerSimulator.class.getResource("item.png")); } catch (Exception e) { } } @Override protected void paintComponent(Graphics g) { if (!isVisible()) { return; } Graphics2D g2 = (Graphics2D) g; setupGraphics(g2); drawBackground(g2); drawItem(g2); } private void drawItem(Graphics2D g2) { double position = time; double xPos = position * getWidth() / 2; int width = getWidth() * 2 / 3; int x = (getWidth() - width) / 2; x += xPos; int y = getHeight() / 2; y -= image.getHeight() / 2; g2.drawImage(image, null, x, y); } private void setupGraphics(Graphics2D g2) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } private void drawBackground(Graphics2D g2) { g2.setColor(COLOR_BACKGROUND); g2.fill(g2.getClipBounds()); } @Override public Dimension getPreferredSize() { return new Dimension(150, 100); } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class DropSimulator extends AbstractSimulator { private static final Color COLOR_BACKGROUND = Color.WHITE; private BufferedImage image; private BufferedImage shadow; private float angle = 90; private int distance = 20; // cached values for fast painting private int distance_x = 0; private int distance_y = 0; public DropSimulator() { try { image = ImageIO.read(BouncerSimulator.class.getResource("icon.png")); ShadowFactory factory = new ShadowFactory(5, 0.5f, Color.BLACK); shadow = factory.createShadow(image); } catch (Exception e) { } } @Override protected void paintComponent(Graphics g) { if (!isVisible()) { return; } Graphics2D g2 = (Graphics2D) g; setupGraphics(g2); drawBackground(g2); drawItem(g2); } private void drawItem(Graphics2D g2) { double position = time; int width = (int) (shadow.getWidth() / 2 * (1.0 + position)); int height = (int) (shadow.getHeight() / 2 * (1.0 + position)); int x = (getWidth() - width) / 2; int y = (getHeight() - height) / 2; Composite composite = g2.getComposite(); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f - (0.5f * (float) position))); computeShadowPosition((position * distance) + 1.0); g2.drawImage(shadow, x + distance_x, y + distance_y, width, height, null); g2.setComposite(composite); width = (int) (image.getWidth() / 2 * (1.0 + position)); height = (int) (image.getHeight() / 2 * (1.0 + position)); x = (getWidth() - width) / 2; y = (getHeight() - height) / 2; g2.drawImage(image, x, y, width, height, null); } private void setupGraphics(Graphics2D g2) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); } private void drawBackground(Graphics2D g2) { g2.setColor(COLOR_BACKGROUND); g2.fill(g2.getClipBounds()); } @Override public Dimension getPreferredSize() { return new Dimension(150, 100); } private void computeShadowPosition(double distance) { double angleRadians = Math.toRadians(angle); distance_x = (int) (Math.cos(angleRadians) * distance); distance_y = (int) (Math.sin(angleRadians) * distance); } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class HeaderPanel extends JPanel { private ImageIcon icon; HeaderPanel(ImageIcon icon, String title, String help1, String help2) { super(new BorderLayout()); this.icon = icon; JPanel titlesPanel = new JPanel(new GridLayout(3, 1)); titlesPanel.setOpaque(false); titlesPanel.setBorder(new EmptyBorder(12, 0, 12, 0)); JLabel headerTitle = new JLabel(title); Font police = headerTitle.getFont().deriveFont(Font.BOLD); headerTitle.setFont(police); headerTitle.setBorder(new EmptyBorder(0, 12, 0, 0)); titlesPanel.add(headerTitle); JLabel message; titlesPanel.add(message = new JLabel(help1)); police = headerTitle.getFont().deriveFont(Font.PLAIN); message.setFont(police); message.setBorder(new EmptyBorder(0, 24, 0, 0)); titlesPanel.add(message = new JLabel(help2)); police = headerTitle.getFont().deriveFont(Font.PLAIN); message.setFont(police); message.setBorder(new EmptyBorder(0, 24, 0, 0)); message = new JLabel(this.icon); message.setBorder(new EmptyBorder(0, 0, 0, 12)); add(BorderLayout.WEST, titlesPanel); add(BorderLayout.EAST, message); add(BorderLayout.SOUTH, new JSeparator(JSeparator.HORIZONTAL)); setPreferredSize(new Dimension(500, this.icon.getIconHeight() + 24)); } public void paintComponent(Graphics g) { super.paintComponent(g); if (!isOpaque()) { return; } Rectangle bounds = g.getClipBounds(); Color control = UIManager.getColor("control"); int width = getWidth(); Graphics2D g2 = (Graphics2D) g; Paint storedPaint = g2.getPaint(); g2.setPaint(new GradientPaint(this.icon.getIconWidth(), 0, Color.white, width, 0, control)); g2.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); g2.setPaint(storedPaint); } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class Java2dHelper { public static BufferedImage createCompatibleImage(int width, int height) { GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice screenDevice = environment.getDefaultScreenDevice(); GraphicsConfiguration configuration = screenDevice.getDefaultConfiguration(); return configuration.createCompatibleImage(width, height); } public static BufferedImage loadCompatibleImage(URL resource) throws IOException { BufferedImage image = ImageIO.read(resource); BufferedImage compatibleImage = createCompatibleImage(image.getWidth(), image.getHeight()); Graphics g = compatibleImage.getGraphics(); g.drawImage(image, 0, 0, null); g.dispose(); image = null; return compatibleImage; } public static BufferedImage createThumbnail(BufferedImage image, int requestedThumbSize) { float ratio = (float) image.getWidth() / (float) image.getHeight(); int width = image.getWidth(); BufferedImage thumb = image; do { width /= 2; if (width < requestedThumbSize) { width = requestedThumbSize; } BufferedImage temp = new BufferedImage(width, (int) (width / ratio), BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = temp.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2.drawImage(thumb, 0, 0, temp.getWidth(), temp.getHeight(), null); g2.dispose(); thumb = temp; } while (width != requestedThumbSize); return thumb; } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * <p>A shadow factory generates a drop shadow for any given picture, respecting * the transparency channel if present. The resulting picture contains the * shadow only and to create a drop shadow effect you will need to stack the * original picture and the shadow generated by the factory. If you are using * Swing you can get this done very easily with the layout * {@link org.jdesktop.swingx.StackLayout}.</p> * <h2>Shadow Properties</h2> * <p>A shadow is defined by three properties: * <ul> * <li><i>size</i>: The size, in pixels, of the shadow. This property also * defines the fuzzyness.</li> * <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li> * <li><i>color</i>: The color of the shadow. Shadows are not meant to be * black only.</li> * </ul> * You can set these properties using the provided mutaters or the appropriate * constructor. Here are two ways of creating a green shadow of size 10 and * with an opacity of 50%: * <pre> * ShadowFactory factory = new ShadowFactory(10, 0.5f, Color.GREEN); * // .. * factory = new ShadowFactory(); * factory.setSize(10); * factory.setOpacity(0.5f); * factory.setColor(Color.GREEN); * </pre> * The default constructor provides the following default values: * <ul> * <li><i>size</i>: 5 pixels</li> * <li><i>opacity</i>: 50%</li> * <li><i>color</i>: Black</li> * </ul></p> * <h2>Shadow Quality</h2> * <p>The factory provides two shadow generation algorithms: <i>fast quality blur</i> * and <i>high quality blur</i>. You can select your preferred algorithm by * setting the appropriate rendering hint: * <pre> * ShadowFactory factory = new ShadowFactory(); * factory.setRenderingHint(ShadowFactory.KEY_BLUR_QUALITY, * ShadowFactory.VALUE_BLUR_QUALITY_HIGH); * </pre> * The default rendering algorihtm is <code>VALUE_BLUR_QUALITY_FAST</code>.</p> * <p>The current implementation should provide the same quality with both * algorithms but performances are guaranteed to be better (about 30 times * faster) with the <i>fast quality blur</i>.</p> * <h2>Generating a Shadow</h2> * <p>A shadow is generated as a <code>BufferedImage</code> from another * <code>BufferedImage</code>. Once the factory is set up, you must call * {@link #createShadow} to actually generate the shadow: * <pre> * ShadowFactory factory = new ShadowFactory(); * // factory setup * BufferedImage shadow = factory.createShadow(bufferedImage); * </pre> * The resulting image is of type <code>BufferedImage.TYPE_INT_ARGB</code>. * Both dimensions of this image are larger than original image's: * <ul> * <li>new width = original width + 2 * shadow size</li> * <li>new height = original height + 2 * shadow size</li> * </ul> * This must be taken into account when you need to create a drop shadow effect.</p> * <h2>Properties Changes</h2> * <p>This factory allows to register property change listeners with * {@link #addPropertyChangeListener}. Listening to properties changes is very * useful when you emebed the factory in a graphical component and give the API * user the ability to access the factory. By listening to properties changes, * you can easily repaint the component when needed.</p> * <h2>Threading Issues</h2> * <p><code>ShadowFactory</code> is not guaranteed to be thread-safe.</p> * * @author Romain Guy <romain.guy@mac.com> * @author Sbastien Petrucci <sebastien_petrucci@yahoo.fr> */ class ShadowFactory { /** * <p>Key for the blur quality rendering hint.</p> */ public static final String KEY_BLUR_QUALITY = "blur_quality"; /** * <p>Selects the fast rendering algorithm. This is the default rendering * hint for <code>KEY_BLUR_QUALITY</code>.</p> */ public static final String VALUE_BLUR_QUALITY_FAST = "fast"; /** * <p>Selects the high quality rendering algorithm. With current implementation, * This algorithm does not guarantee a better rendering quality and should * not be used.</p> */ public static final String VALUE_BLUR_QUALITY_HIGH = "high"; /** * <p>Identifies a change to the size used to render the shadow.</p> * <p>When the property change event is fired, the old value and the new * value are provided as <code>Integer</code> instances.</p> */ public static final String SIZE_CHANGED_PROPERTY = "shadow_size"; /** * <p>Identifies a change to the opacity used to render the shadow.</p> * <p>When the property change event is fired, the old value and the new * value are provided as <code>Float</code> instances.</p> */ public static final String OPACITY_CHANGED_PROPERTY = "shadow_opacity"; /** * <p>Identifies a change to the color used to render the shadow.</p> */ public static final String COLOR_CHANGED_PROPERTY = "shadow_color"; // size of the shadow in pixels (defines the fuzziness) private int size = 5; // opacity of the shadow private float opacity = 0.5f; // color of the shadow private Color color = Color.BLACK; // rendering hints map private HashMap<Object, Object> hints; // notifies listeners of properties changes private PropertyChangeSupport changeSupport; /** * <p>Creates a default good looking shadow generator. * The default shadow factory provides the following default values: * <ul> * <li><i>size</i>: 5 pixels</li> * <li><i>opacity</i>: 50%</li> * <li><i>color</i>: Black</li> * <li><i>rendering quality</i>: VALUE_BLUR_QUALITY_FAST</li> * </ul></p> * <p>These properties provide a regular, good looking shadow.</p> */ public ShadowFactory() { this(5, 0.5f, Color.BLACK); } /** * <p>A shadow factory needs three properties to generate shadows. * These properties are:</p> * <ul> * <li><i>size</i>: The size, in pixels, of the shadow. This property also * defines the fuzzyness.</li> * <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li> * <li><i>color</i>: The color of the shadow. Shadows are not meant to be * black only.</li> * </ul></p> * <p>Besides these properties you can set rendering hints to control the * rendering process. The default rendering hints let the factory use the * fastest shadow generation algorithm.</p> * @param size The size of the shadow in pixels. Defines the fuzziness. * @param opacity The opacity of the shadow. * @param color The color of the shadow. * @see #setRenderingHint(Object, Object) */ public ShadowFactory(final int size, final float opacity, final Color color) { hints = new HashMap<Object, Object>(); hints.put(KEY_BLUR_QUALITY, VALUE_BLUR_QUALITY_FAST); changeSupport = new PropertyChangeSupport(this); setSize(size); setOpacity(opacity); setColor(color); } /** * <p>Add a PropertyChangeListener to the listener list. The listener is * registered for all properties. The same listener object may be added * more than once, and will be called as many times as it is added. If * <code>listener</code> is null, no exception is thrown and no action * is taken.</p> * @param listener the PropertyChangeListener to be added */ public void addPropertyChangeListener(PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(listener); } /** * <p>Remove a PropertyChangeListener from the listener list. This removes * a PropertyChangeListener that was registered for all properties. If * <code>listener</code> was added more than once to the same event source, * it will be notified one less time after being removed. If * <code>listener</code> is null, or was never added, no exception is thrown * and no action is taken.</p> * @param listener */ public void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } /** * <p>Maps the specified rendering hint <code>key</code> to the specified * <code>value</code> in this <code>SahdowFactory</code> object.</p> * @param key The rendering hint key * @param value The rendering hint value */ public void setRenderingHint(final Object key, final Object value) { hints.put(key, value); } /** * <p>Gets the color used by the factory to generate shadows.</p> * @return this factory's shadow color */ public Color getColor() { return color; } /** * <p>Sets the color used by the factory to generate shadows.</p> * <p>Consecutive calls to {@link #createShadow} will all use this color * until it is set again.</p> * <p>If the color provided is null, the previous color will be retained.</p> * @param shadowColor the generated shadows color */ public void setColor(final Color shadowColor) { if (shadowColor != null) { Color oldColor = this.color; this.color = shadowColor; changeSupport.firePropertyChange(COLOR_CHANGED_PROPERTY, oldColor, this.color); } } /** * <p>Gets the opacity used by the factory to generate shadows.</p> * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully * transparent and 1.0f fully opaque.</p> * @return this factory's shadow opacity */ public float getOpacity() { return opacity; } /** * <p>Sets the opacity used by the factory to generate shadows.</p> * <p>Consecutive calls to {@link #createShadow} will all use this color * until it is set again.</p> * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully * transparent and 1.0f fully opaque. If you provide a value out of these * boundaries, it will be restrained to the closest boundary.</p> * @param shadowOpacity the generated shadows opacity */ public void setOpacity(final float shadowOpacity) { float oldOpacity = this.opacity; if (shadowOpacity < 0.0) { this.opacity = 0.0f; } else if (shadowOpacity > 1.0f) { this.opacity = 1.0f; } else { this.opacity = shadowOpacity; } changeSupport.firePropertyChange(OPACITY_CHANGED_PROPERTY, new Float(oldOpacity), new Float(this.opacity)); } /** * <p>Gets the size in pixel used by the factory to generate shadows.</p> * @return this factory's shadow size */ public int getSize() { return size; } /** * <p>Sets the size, in pixels, used by the factory to generate shadows.</p> * <p>The size defines the blur radius applied to the shadow to create the * fuzziness.</p> * <p>There is virtually no limit to the size but it has an impact on shadow * generation performances. The greater this value, the longer it will take * to generate the shadow. Remember the generated shadow image dimensions * are computed as follow: * <ul> * <li>new width = original width + 2 * shadow size</li> * <li>new height = original height + 2 * shadow size</li> * </ul> * The size cannot be negative. If you provide a negative value, the size * will be 0 instead.</p> * @param shadowSize the generated shadows size in pixels (fuzziness) */ public void setSize(final int shadowSize) { int oldSize = this.size; if (shadowSize < 0) { this.size = 0; } else { this.size = shadowSize; } changeSupport.firePropertyChange(SIZE_CHANGED_PROPERTY, new Integer(oldSize), new Integer(this.size)); } /** * <p>Generates the shadow for a given picture and the current properties * of the factory.</p> * <p>The generated shadow image dimensions are computed as follow: * <ul> * <li>new width = original width + 2 * shadow size</li> * <li>new height = original height + 2 * shadow size</li> * </ul></p> * <p>The time taken by a call to this method depends on the size of the * shadow, the larger the longer it takes, and on the selected rendering * algorithm.</p> * @param image the picture from which the shadow must be cast * @return the picture containing the shadow of <code>image</code> */ public BufferedImage createShadow(final BufferedImage image) { if (hints.get(KEY_BLUR_QUALITY) == VALUE_BLUR_QUALITY_HIGH) { // the high quality algorithm is a 3-pass algorithm // it goes through all the pixels of the original picture at least // three times to generate the shadow // it is easy to understand but very slow BufferedImage subject = prepareImage(image); BufferedImage shadow = new BufferedImage(subject.getWidth(), subject.getHeight(), BufferedImage.TYPE_INT_ARGB); BufferedImage shadowMask = createShadowMask(subject); getLinearBlurOp(size).filter(shadowMask, shadow); return shadow; } // call the fast rendering algorithm return createShadowFast(image); } // prepares the picture for the high quality rendering algorithm private BufferedImage prepareImage(final BufferedImage image) { BufferedImage subject = new BufferedImage(image.getWidth() + size * 2, image.getHeight() + size * 2, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = subject.createGraphics(); g2.drawImage(image, null, size, size); g2.dispose(); return subject; } // fast rendering algorithm // basically applies duplicates the picture and applies a size*size kernel // in only one pass. // the kernel is simulated by an horizontal and a vertical pass // implemented by Sbastien Petrucci private BufferedImage createShadowFast(final BufferedImage src) { int shadowSize = this.size; int srcWidth = src.getWidth(); int srcHeight = src.getHeight(); int dstWidth = srcWidth + size; int dstHeight = srcHeight + size; int left = (shadowSize - 1) >> 1; int right = shadowSize - left; int yStop = dstHeight - right; BufferedImage dst = new BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB); int shadowRgb = color.getRGB() & 0x00FFFFFF; int[] aHistory = new int[shadowSize]; int historyIdx; int aSum; ColorModel srcColorModel = src.getColorModel(); WritableRaster srcRaster = src.getRaster(); int[] dstBuffer = ((DataBufferInt) dst.getRaster().getDataBuffer()).getData(); int lastPixelOffset = right * dstWidth; float hSumDivider = 1.0f / size; float vSumDivider = opacity / size; // horizontal pass : extract the alpha mask from the source picture and // blur it into the destination picture for (int srcY = 0, dstOffset = left * dstWidth; srcY < srcHeight; srcY++) { // first pixels are empty for (historyIdx = 0; historyIdx < shadowSize; ) { aHistory[historyIdx++] = 0; } aSum = 0; historyIdx = 0; // compute the blur average with pixels from the source image for (int srcX = 0; srcX < srcWidth; srcX++) { int a = (int) (aSum * hSumDivider); // calculate alpha value dstBuffer[dstOffset++] = a << 24; // store the alpha value only // the shadow color will be added in the next pass aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum // extract the new pixel ... a = srcColorModel.getAlpha(srcRaster.getDataElements(srcX, srcY, null)); aHistory[historyIdx] = a; // ... and store its value into history aSum += a; // ... and add its value to the sum if (++historyIdx >= shadowSize) { historyIdx -= shadowSize; } } // blur the end of the row - no new pixels to grab for (int i = 0; i < shadowSize; i++) { int a = (int) (aSum * hSumDivider); dstBuffer[dstOffset++] = a << 24; // substract the oldest pixel from the sum ... and nothing new to add ! aSum -= aHistory[historyIdx]; if (++historyIdx >= shadowSize) { historyIdx -= shadowSize; } } } // vertical pass for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) { aSum = 0; // first pixels are empty for (historyIdx = 0; historyIdx < left;) { aHistory[historyIdx++] = 0; } // and then they come from the dstBuffer for (int y = 0; y < right; y++, bufferOffset += dstWidth) { int a = dstBuffer[bufferOffset] >>> 24; // extract alpha aHistory[historyIdx++] = a; // store into history aSum += a; // and add to sum } bufferOffset = x; historyIdx = 0; // compute the blur average with pixels from the previous pass for (int y = 0; y < yStop; y++, bufferOffset += dstWidth) { int a = (int) (aSum * vSumDivider); // calculate alpha value dstBuffer[bufferOffset] = a << 24 | shadowRgb; // store alpha value + shadow color aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum a = dstBuffer[bufferOffset + lastPixelOffset] >>> 24; // extract the new pixel ... aHistory[historyIdx] = a; // ... and store its value into history aSum += a; // ... and add its value to the sum if (++historyIdx >= shadowSize) { historyIdx -= shadowSize; } } // blur the end of the column - no pixels to grab anymore for (int y = yStop; y < dstHeight; y++, bufferOffset += dstWidth) { int a = (int) (aSum * vSumDivider); dstBuffer[bufferOffset] = a << 24 | shadowRgb; aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum if (++historyIdx >= shadowSize) { historyIdx -= shadowSize; } } } return dst; } // creates the shadow mask for the original picture // it colorize all the pixels with the shadow color according to their // original transparency private BufferedImage createShadowMask(final BufferedImage image) { BufferedImage mask = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = mask.createGraphics(); g2d.drawImage(image, 0, 0, null); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, opacity)); g2d.setColor(color); g2d.fillRect(0, 0, image.getWidth(), image.getHeight()); g2d.dispose(); return mask; } // creates a blur convolve operation by generating a kernel of // dimensions (size, size). private ConvolveOp getLinearBlurOp(final int size) { float[] data = new float[size * size]; float value = 1.0f / (float) (size * size); for (int i = 0; i < data.length; i++) { data[i] = value; } return new ConvolveOp(new Kernel(size, size, data)); } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class SplineControlPanel extends JPanel { private SplineDisplay display; private DropSimulator dropSimulator = new DropSimulator(); private BouncerSimulator bounceSimulator = new BouncerSimulator(); private int linesCount = 0; private JLabel labelControl1; private JLabel labelControl2; private Animator controller; SplineControlPanel() { super(new BorderLayout()); add(buildEquationDisplay(), BorderLayout.CENTER); add(buildDebugControls(), BorderLayout.EAST); } private Component buildDebugControls() { JButton button; JPanel debugPanel = new JPanel(new GridBagLayout()); debugPanel.add(Box.createHorizontalStrut(150), new GridBagConstraints(0, linesCount++, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); // button = addButton(debugPanel, "Create"); // button.addActionListener(new ActionListener() { // public void actionPerformed(ActionEvent e) { // JFileChooser chooser = new JFileChooser("."); // int choice = chooser.showSaveDialog(SplineControlPanel.this); // if (choice == JFileChooser.CANCEL_OPTION) { // return; // } // File file = chooser.getSelectedFile(); // try { // OutputStream out = new FileOutputStream(file); // display.saveAsTemplate(out); // out.close(); // } catch (FileNotFoundException e1) { // } catch (IOException e1) { // } // } // }); addSeparator(debugPanel, "Control Points"); labelControl1 = addDebugLabel(debugPanel, "Point 1:", formatPoint(display.getControl1())); labelControl2 = addDebugLabel(debugPanel, "Point 2:", formatPoint(display.getControl2())); button = addButton(debugPanel, "Copy Code"); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { NumberFormat formatter = getNumberFormatter(); Point2D c1 = display.getControl1(); Point2D c2 = display.getControl2(); StringBuilder code = new StringBuilder(); code.append("Spline spline = new Spline("); code.append(formatter.format(c1.getX())).append("f, "); code.append(formatter.format(c1.getY())).append("f, "); code.append(formatter.format(c2.getX())).append("f, "); code.append(formatter.format(c2.getY())).append("f);"); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(new StringSelection(code.toString()), null); } }); addEmptySpace(debugPanel, 6); addSeparator(debugPanel, "Animation"); button = addButton(debugPanel, "Play Sample"); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { startSampleAnimation(); } }); addEmptySpace(debugPanel, 6); addSeparator(debugPanel, "Templates"); debugPanel.add(createTemplates(), new GridBagConstraints(0, linesCount++, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); addEmptySpace(debugPanel, 6); debugPanel.add(Box.createVerticalGlue(), new GridBagConstraints(0, linesCount++, 2, 1, 1.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); JPanel wrapper = new JPanel(new BorderLayout()); wrapper.add(new JSeparator(JSeparator.VERTICAL), BorderLayout.WEST); wrapper.add(debugPanel); wrapper.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 6)); return wrapper; } private Component createTemplates() { DefaultListModel model = new DefaultListModel(); model.addElement(createTemplate(0.0, 0.0, 1.0, 1.0)); model.addElement(createTemplate(0.0, 1.0, 0.0, 1.0)); model.addElement(createTemplate(0.0, 1.0, 1.0, 1.0)); model.addElement(createTemplate(0.0, 1.0, 1.0, 0.0)); model.addElement(createTemplate(1.0, 0.0, 0.0, 1.0)); model.addElement(createTemplate(1.0, 0.0, 1.0, 1.0)); model.addElement(createTemplate(1.0, 0.0, 1.0, 0.0)); JList list = new JList(model); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setCellRenderer(new TemplateCellRenderer()); list.addListSelectionListener(new TemplateSelectionHandler()); JScrollPane pane = new JScrollPane(list); pane.getViewport().setPreferredSize(new Dimension(98, 97 * 3)); return pane; } private JButton addButton(JPanel debugPanel, String label) { JButton button; debugPanel.add(button = new JButton(label), new GridBagConstraints(0, linesCount++, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(3, 0, 0, 0), 0, 0)); return button; } private String formatPoint(Point2D p) { NumberFormat formatter = getNumberFormatter(); return "" + formatter.format(p.getX()) + ", " + formatter.format(p.getY()); } private Component buildEquationDisplay() { JPanel panel = new JPanel(new BorderLayout()); display = new SplineDisplay(); display.addPropertyChangeListener("control1", new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { labelControl1.setText(formatPoint(display.getControl1())); } }); display.addPropertyChangeListener("control2", new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { labelControl2.setText(formatPoint(display.getControl2())); } }); panel.add(display, BorderLayout.NORTH); JPanel wrapper = new JPanel(new GridBagLayout()); wrapper.add(new JSeparator(), new GridBagConstraints(0, 0, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0)); wrapper.add(bounceSimulator, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); wrapper.add(dropSimulator, new GridBagConstraints(1, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); panel.add(wrapper, BorderLayout.CENTER); return panel; } private JLabel addDebugLabel(JPanel panel, String label, String value) { JLabel labelComponent = new JLabel(label); panel.add(labelComponent, new GridBagConstraints(0, linesCount, 1, 1, 0.5, 0.0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, new Insets(0, 6, 0, 0), 0, 0)); labelComponent = new JLabel(value); panel.add(labelComponent, new GridBagConstraints(1, linesCount++, 1, 1, 0.5, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 6, 0, 0), 0, 0)); return labelComponent; } private void addEmptySpace(JPanel panel, int size) { panel.add(Box.createVerticalStrut(size), new GridBagConstraints(0, linesCount++, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.VERTICAL, new Insets(6, 0, 0, 0), 0, 0)); } private void addSeparator(JPanel panel, String label) { JPanel innerPanel = new JPanel(new GridBagLayout()); innerPanel.add(new JLabel(label), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); innerPanel.add(new JSeparator(), new GridBagConstraints(1, 0, 1, 1, 0.9, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 6, 0, 6), 0, 0)); panel.add(innerPanel, new GridBagConstraints(0, linesCount++, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 6, 0), 0, 0)); } private void startSampleAnimation() { if (controller != null && controller.isRunning()) { controller.stop(); } Point2D control1 = display.getControl1(); Point2D control2 = display.getControl2(); Interpolator splines = new SplineInterpolator((float) control1.getX(), (float) control1.getY(), (float) control2.getX(), (float) control2.getY()); KeyTimes times = new KeyTimes(0.0f, 1.0f); KeyValues values = KeyValues.create(0.0, 1.0); KeyFrames frames = new KeyFrames(values,times, splines); PropertySetter dropModifier = new PropertySetter(dropSimulator, "time", frames); PropertySetter bounceModifier = new PropertySetter(bounceSimulator, "time", frames); controller = new Animator(1000, 4, RepeatBehavior.REVERSE, dropModifier); controller.setResolution(10); controller.addTarget(bounceModifier); controller.start(); } private Evaluator point2dInterpolator = new Point2DNonLinearInterpolator(); private class Point2DNonLinearInterpolator extends Evaluator<Point2D> { private Point2D value; public Point2D evaluate(Point2D v0, Point2D v1, float fraction) { Point2D value = (Point2D)v0.clone(); if (v0 != v1) { double x = value.getX(); x += (v1.getX() - v0.getX()) * fraction; double y = value.getY(); y += (v1.getY() - v0.getY()) * fraction; value.setLocation(x, y); } else { value.setLocation(v0.getX(), v0.getY()); } return value; } } private class TemplateSelectionHandler implements ListSelectionListener { public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { return; } JList list = (JList) e.getSource(); Template template = (Template) list.getSelectedValue(); if (template != null) { if (controller != null && controller.isRunning()) { controller.stop(); } controller = new Animator(300, new PropertySetter(display, "control1", point2dInterpolator, display.getControl1(), template.getControl1())); controller.setResolution(10); controller.addTarget(new PropertySetter(display, "control2", point2dInterpolator, display.getControl2(), template.getControl2())); controller.start(); } } } private static NumberFormat getNumberFormatter() { NumberFormat formatter = NumberFormat.getInstance(Locale.ENGLISH); formatter.setMinimumFractionDigits(2); formatter.setMaximumFractionDigits(2); return formatter; } private static Template createTemplate(double x1, double y1, double x2, double y2) { return new Template(new Point2D.Double(x1, y1), new Point2D.Double(x2, y2)); } private static class TemplateCellRenderer extends DefaultListCellRenderer { private boolean isSelected; @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Template template = (Template) value; this.setBackground(Color.WHITE); this.setIcon(new ImageIcon(template.getImage())); this.isSelected = isSelected; return this; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (isSelected) { g.setColor(new Color(0.0f, 0.0f, 0.7f, 0.1f)); g.fillRect(0, 0, getWidth(), getHeight()); } } } private static class Template { private Point2D control1; private Point2D control2; private Image image; public Template(Point2D control1, Point2D control2) { this.control1 = control1; this.control2 = control2; } public Point2D getControl1() { return control1; } public Point2D getControl2() { return control2; } public Image getImage() { if (image == null) { NumberFormat formatter = getNumberFormatter(); String name = ""; name += formatter.format(control1.getX()) + '-' + formatter.format(control1.getY()); name += '-'; name += formatter.format(control2.getX()) + '-' + formatter.format(control2.getY()); try { image = ImageIO.read(getClass().getResourceAsStream(name + ".png")); } catch (IOException e) { } } return image; } } } /** * Copyright (c) 2006, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class SplineDisplay extends EquationDisplay { private static final double CONTROL_POINT_SIZE = 12.0; private Point2D control1 = new Point2D.Double(0.25, 0.75); private Point2D control2 = new Point2D.Double(0.75, 0.25); private Point2D selected = null; private Point dragStart = null; private boolean isSaving = false; private PropertyChangeSupport support; SplineDisplay() { super(0.0, 0.0, -0.1, 1.1, -0.1, 1.1, 0.2, 6, 0.2, 6); setEnabled(false); addMouseMotionListener(new ControlPointsHandler()); addMouseListener(new SelectionHandler()); support = new PropertyChangeSupport(this); } public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { support.addPropertyChangeListener(propertyName, listener); } public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { support.removePropertyChangeListener(propertyName, listener); } public Point2D getControl1() { return (Point2D) control1.clone(); } public Point2D getControl2() { return (Point2D) control2.clone(); } public void setControl1(Point2D control1) { support.firePropertyChange("control1", (Point2D) this.control1.clone(), (Point2D) control1.clone()); this.control1 = (Point2D) control1.clone(); repaint(); } public void setControl2(Point2D control2) { support.firePropertyChange("control2", (Point2D) this.control2.clone(), (Point2D) control2.clone()); this.control2 = (Point2D) control2.clone(); repaint(); } synchronized void saveAsTemplate(OutputStream out) { BufferedImage image = Java2dHelper.createCompatibleImage(getWidth(), getHeight()); Graphics g = image.getGraphics(); isSaving = true; setDrawText(false); paint(g); setDrawText(true); isSaving = false; g.dispose(); BufferedImage subImage = image.getSubimage((int) xPositionToPixel(0.0), (int) yPositionToPixel(1.0), (int) (xPositionToPixel(1.0) - xPositionToPixel(0.0)) + 1, (int) (yPositionToPixel(0.0) - yPositionToPixel(1.0)) + 1); try { ImageIO.write(subImage, "PNG", out); } catch (IOException e) { } image.flush(); subImage = null; image = null; } @Override protected void paintInformation(Graphics2D g2) { if (!isSaving) { paintControlPoints(g2); } paintSpline(g2); } private void paintControlPoints(Graphics2D g2) { paintControlPoint(g2, control1); paintControlPoint(g2, control2); } private void paintControlPoint(Graphics2D g2, Point2D control) { double origin_x = xPositionToPixel(control.getX()); double origin_y = yPositionToPixel(control.getY()); double pos = control == control1 ? 0.0 : 1.0; Ellipse2D outer = getDraggableArea(control); Ellipse2D inner = new Ellipse2D.Double(origin_x + 2.0 - CONTROL_POINT_SIZE / 2.0, origin_y + 2.0 - CONTROL_POINT_SIZE / 2.0, 8.0, 8.0); Area circle = new Area(outer); circle.subtract(new Area(inner)); Stroke stroke = g2.getStroke(); g2.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 5, new float[] { 5, 5 }, 0)); g2.setColor(new Color(1.0f, 0.0f, 0.0f, 0.4f)); g2.drawLine(0, (int) origin_y, (int) origin_x, (int) origin_y); g2.drawLine((int) origin_x, (int) origin_y, (int) origin_x, getHeight()); g2.setStroke(stroke); if (selected == control) { g2.setColor(new Color(1.0f, 1.0f, 1.0f, 1.0f)); } else { g2.setColor(new Color(0.8f, 0.8f, 0.8f, 0.6f)); } g2.fill(inner); g2.setColor(new Color(0.0f, 0.0f, 0.5f, 0.5f)); g2.fill(circle); g2.drawLine((int) origin_x, (int) origin_y, (int) xPositionToPixel(pos), (int) yPositionToPixel(pos)); } private Ellipse2D getDraggableArea(Point2D control) { Ellipse2D outer = new Ellipse2D.Double(xPositionToPixel(control.getX()) - CONTROL_POINT_SIZE / 2.0, yPositionToPixel(control.getY()) - CONTROL_POINT_SIZE / 2.0, CONTROL_POINT_SIZE, CONTROL_POINT_SIZE); return outer; } private void paintSpline(Graphics2D g2) { CubicCurve2D spline = new CubicCurve2D.Double(xPositionToPixel(0.0), yPositionToPixel(0.0), xPositionToPixel(control1.getX()), yPositionToPixel(control1.getY()), xPositionToPixel(control2.getX()), yPositionToPixel(control2.getY()), xPositionToPixel(1.0), yPositionToPixel(1.0)); g2.setColor(new Color(0.0f, 0.3f, 0.0f, 1.0f)); g2.draw(spline); } private void resetSelection() { Point2D oldSelected = selected; selected = null; if (oldSelected != null) { Rectangle bounds = getDraggableArea(oldSelected).getBounds(); repaint(bounds.x, bounds.y, bounds.width, bounds.height); } } private class ControlPointsHandler extends MouseMotionAdapter { @Override public void mouseMoved(MouseEvent e) { Ellipse2D area1 = getDraggableArea(control1); Ellipse2D area2 = getDraggableArea(control2); if (area1.contains(e.getPoint()) || area2.contains(e.getPoint())) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { setCursor(Cursor.getDefaultCursor()); } } @Override public void mouseDragged(MouseEvent e) { if (selected == null) { return; } Point dragEnd = e.getPoint(); double distance = xPixelToPosition(dragEnd.getX()) - xPixelToPosition(dragStart.getX()); double x = selected.getX() + distance; if (x < 0.0) { x = 0.0; } else if (x > 1.0) { x = 1.0; } distance = yPixelToPosition(dragEnd.getY()) - yPixelToPosition(dragStart.getY()); double y = selected.getY() + distance; if (y < 0.0) { y = 0.0; } else if (y > 1.0) { y = 1.0; } Point2D selectedCopy = (Point2D) selected.clone(); selected.setLocation(x, y); support.firePropertyChange("control" + (selected == control1 ? "1" : "2"), selectedCopy, (Point2D) selected.clone()); repaint(); double xPos = xPixelToPosition(dragEnd.getX()); double yPos = -yPixelToPosition(dragEnd.getY()); if (xPos >= 0.0 && xPos <= 1.0) { dragStart.setLocation(dragEnd.getX(), dragStart.getY()); } if (yPos >= 0.0 && yPos <= 1.0) { dragStart.setLocation(dragStart.getX(), dragEnd.getY()); } } } private class SelectionHandler extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { Ellipse2D area1 = getDraggableArea(control1); Ellipse2D area2 = getDraggableArea(control2); if (area1.contains(e.getPoint())) { selected = control1; dragStart = e.getPoint(); Rectangle bounds = area1.getBounds(); repaint(bounds.x, bounds.y, bounds.width, bounds.height); } else if (area2.contains(e.getPoint())) { selected = control2; dragStart = e.getPoint(); Rectangle bounds = area2.getBounds(); repaint(bounds.x, bounds.y, bounds.width, bounds.height); } else { resetSelection(); } } @Override public void mouseReleased(MouseEvent e) { resetSelection(); } } }