From 1293e2a07488977df27bbd3bfff68c88a9bded44 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 13 Nov 2020 13:31:11 +0100 Subject: [PATCH] AnimatedIcon added (for future animations) (issue #66) --- .../flatlaf/icons/FlatAnimatedIcon.java | 55 ++++ .../formdev/flatlaf/util/AnimatedIcon.java | 249 +++++++++++++++++ .../formdev/flatlaf/util/ColorFunctions.java | 32 +++ .../flatlaf/util/CubicBezierEasing.java | 7 + .../flatlaf/testing/FlatAnimatedIconTest.java | 254 ++++++++++++++++++ .../flatlaf/testing/FlatAnimatedIconTest.jfd | 72 +++++ 6 files changed, 669 insertions(+) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java new file mode 100644 index 00000000..4d80f49b --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.icons; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import com.formdev.flatlaf.util.AnimatedIcon; + +/** + * Base class for animated icons that scales width and height, creates and initializes + * a scaled graphics context for icon painting. + *

+ * Subclasses do not need to scale icon painting. + *

+ * This class does not store any state information (needed for animation) in its instance. + * Instead a client property is set on the painted component. + * This makes it possible to use a share icon instance for multiple components. + * + * @author Karl Tauber + */ +public abstract class FlatAnimatedIcon + extends FlatAbstractIcon + implements AnimatedIcon +{ + public FlatAnimatedIcon( int width, int height, Color color ) { + super( width, height, color ); + } + + @Override + public void paintIcon( Component c, Graphics g, int x, int y ) { + super.paintIcon( c, g, x, y ); + AnimatedIcon.AnimationSupport.saveIconLocation( this, c, x, y ); + } + + @Override + protected void paintIcon( Component c, Graphics2D g ) { + AnimatedIcon.AnimationSupport.paintIcon( this, c, g, 0, 0 ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java new file mode 100644 index 00000000..da0b0a1a --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java @@ -0,0 +1,249 @@ +/* + * Copyright 2020 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import java.awt.Component; +import java.awt.Graphics; +import javax.swing.Icon; +import javax.swing.JComponent; +import com.formdev.flatlaf.util.Animator.Interpolator; + +/** + * Icon that automatically animates painting on component value changes. + *

+ * {@link #getValue(Component)} returns the value of the component. + * If the value changes, then {@link #paintIconAnimated(Component, Graphics, int, int, float)} + * is invoked multiple times with animated value (from old value to new value). + *

+ * Example for an animated icon: + *

+ * private class AnimatedMinimalTestIcon
+ *     implements AnimatedIcon
+ * {
+ *     @Override public int getIconWidth() { return 100; }
+ *     @Override public int getIconHeight() { return 20; }
+ *
+ *     @Override
+ *     public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) {
+ *         int w = getIconWidth();
+ *         int h = getIconHeight();
+ *
+ *         g.setColor( Color.red );
+ *         g.drawRect( x, y, w - 1, h - 1 );
+ *         g.fillRect( x, y, Math.round( w * animatedValue ), h );
+ *     }
+ *
+ *     @Override
+ *     public float getValue( Component c ) {
+ *         return ((AbstractButton)c).isSelected() ? 1 : 0;
+ *     }
+ * }
+ *
+ * // sample usage
+ * JCheckBox checkBox = new JCheckBox( "test" );
+ * checkBox.setIcon( new AnimatedMinimalTestIcon() );
+ * 
+ * + * Animation works only if the component passed to {@link #paintIcon(Component, Graphics, int, int)} + * is a instance of {@link JComponent}. + * A client property is set on the component to store the animation state. + * + * @author Karl Tauber + */ +public interface AnimatedIcon + extends Icon +{ + @Override + public default void paintIcon( Component c, Graphics g, int x, int y ) { + AnimationSupport.paintIcon( this, c, g, x, y ); + } + + /** + * Paints the icon for the given animated value. + * + * @param c the component that this icon belongs to + * @param g the graphics context + * @param x the x coordinate of the icon + * @param y the y coordinate of the icon + * @param animatedValue the animated value, which is either equal to what {@link #getValue(Component)} + * returned, or somewhere between the previous value and the latest value + * that {@link #getValue(Component)} returned + */ + void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ); + + /** + * Gets the value of the component. + *

+ * This can be any value and depends on the component. + * If the value changes, then this class animates from the old value to the new one. + *

+ * For a toggle button this could be {@code 0} for off and {@code 1} for on. + */ + float getValue( Component c ); + + /** + * Returns whether animation is enabled for this icon (default is {@code true}). + */ + default boolean isAnimationEnabled() { + return true; + } + + /** + * Returns the duration of the animation in milliseconds (default is 150). + */ + default int getAnimationDuration() { + return 150; + } + + /** + * Returns the resolution of the animation in milliseconds (default is 10). + * Resolution is the amount of time between timing events. + */ + default int getAnimationResolution() { + return 10; + } + + /** + * Returns the interpolator for the animation. + * Default is {@link CubicBezierEasing#STANDARD_EASING}. + */ + default Interpolator getAnimationInterpolator() { + return CubicBezierEasing.STANDARD_EASING; + } + + /** + * Returns the client property key used to store the animation support. + */ + default Object getClientPropertyKey() { + return getClass(); + } + + //---- class AnimationSupport --------------------------------------------- + + /** + * Animation support class that stores the animation state and implements the animation. + */ + class AnimationSupport + { + private float startValue; + private float targetValue; + private float animatedValue; + private float fraction; + + private Animator animator; + + // last x,y coordinates of the icon needed to repaint while animating + private int x; + private int y; + + public static void paintIcon( AnimatedIcon icon, Component c, Graphics g, int x, int y ) { + if( !isAnimationEnabled( icon, c ) ) { + // paint without animation if animation is disabled or + // component is not a JComponent and therefore does not support + // client properties, which are required to keep animation state + paintIconImpl( icon, c, g, x, y, null ); + return; + } + + JComponent jc = (JComponent) c; + Object key = icon.getClientPropertyKey(); + AnimationSupport as = (AnimationSupport) jc.getClientProperty( key ); + if( as == null ) { + // painted first time --> do not animate, but remember current component value + as = new AnimationSupport(); + as.startValue = as.targetValue = as.animatedValue = icon.getValue( c ); + as.x = x; + as.y = y; + jc.putClientProperty( key, as ); + } else { + // get component value + float value = icon.getValue( c ); + + if( value != as.targetValue ) { + // value changed --> (re)start animation + + if( as.animator == null ) { + // create animator + AnimationSupport as2 = as; + as.animator = new Animator( icon.getAnimationDuration(), fraction -> { + // check whether component was removed while animation is running + if( !c.isDisplayable() ) { + as2.animator.stop(); + return; + } + + // compute animated value + as2.animatedValue = as2.startValue + ((as2.targetValue - as2.startValue) * fraction); + as2.fraction = fraction; + + // repaint icon + c.repaint( as2.x, as2.y, icon.getIconWidth(), icon.getIconHeight() ); + }, () -> { + as2.startValue = as2.animatedValue = as2.targetValue; + as2.animator = null; + } ); + } + + if( as.animator.isRunning() ) { + // if animation is still running, restart it from the current + // animated value to the new target value with reduced duration + as.animator.cancel(); + int duration2 = (int) (icon.getAnimationDuration() * as.fraction); + if( duration2 > 0 ) + as.animator.setDuration( duration2 ); + as.startValue = as.animatedValue; + } else { + // new animation + as.animator.setDuration( icon.getAnimationDuration() ); + as.animator.setResolution( icon.getAnimationResolution() ); + as.animator.setInterpolator( icon.getAnimationInterpolator() ); + + as.animatedValue = as.startValue; + } + + as.targetValue = value; + as.animator.start(); + } + + as.x = x; + as.y = y; + } + + paintIconImpl( icon, c, g, x, y, as ); + } + + private static void paintIconImpl( AnimatedIcon icon, Component c, Graphics g, int x, int y, AnimationSupport as ) { + float value = (as != null) ? as.animatedValue : icon.getValue( c ); + icon.paintIconAnimated( c, g, x, y, value ); + } + + private static boolean isAnimationEnabled( AnimatedIcon icon, Component c ) { + return Animator.useAnimation() && icon.isAnimationEnabled() && c instanceof JComponent; + } + + public static void saveIconLocation( AnimatedIcon icon, Component c, int x, int y ) { + if( !isAnimationEnabled( icon, c ) ) + return; + + AnimationSupport as = (AnimationSupport) ((JComponent)c).getClientProperty( icon.getClientPropertyKey() ); + if( as != null ) { + as.x = x; + as.y = y; + } + } + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java index 4996ba75..3e4e5963 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java @@ -44,6 +44,38 @@ public class ColorFunctions : value); } + /** + * Returns a color that is a mixture of two colors. + * + * @param color1 first color + * @param color2 second color + * @param weight the weight (in range 0-1) to mix the two colors. + * Larger weight uses more of first color, smaller weight more of second color. + * @return mixture of colors + */ + public static Color mix( Color color1, Color color2, float weight ) { + if( weight >= 1 ) + return color1; + if( weight <= 0 ) + return color2; + + int r1 = color1.getRed(); + int g1 = color1.getGreen(); + int b1 = color1.getBlue(); + int a1 = color1.getAlpha(); + + int r2 = color2.getRed(); + int g2 = color2.getGreen(); + int b2 = color2.getBlue(); + int a2 = color2.getAlpha(); + + return new Color( + Math.round( r2 + ((r1 - r2) * weight) ), + Math.round( g2 + ((g1 - g2) * weight) ), + Math.round( b2 + ((b1 - b2) * weight) ), + Math.round( a2 + ((a1 - a2) * weight) ) ); + } + //---- interface ColorFunction -------------------------------------------- public interface ColorFunction { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/CubicBezierEasing.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/CubicBezierEasing.java index e714a209..b9336c98 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/CubicBezierEasing.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/CubicBezierEasing.java @@ -24,6 +24,13 @@ package com.formdev.flatlaf.util; public class CubicBezierEasing implements Animator.Interpolator { + /** + * Standard easing as specified in Material design (0.4, 0, 0.2, 1). + * + * @see https://material.io/design/motion/speed.html#easing + */ + public static final CubicBezierEasing STANDARD_EASING = new CubicBezierEasing( 0.4f, 0f, 0.2f, 1f ); + // common cubic-bezier easing functions (same as in CSS) // https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function public static final CubicBezierEasing EASE = new CubicBezierEasing( 0.25f, 0.1f, 0.25f, 1f ); diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java new file mode 100644 index 00000000..4502d6b2 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java @@ -0,0 +1,254 @@ +/* + * Copyright 2020 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.Ellipse2D; +import javax.swing.*; +import com.formdev.flatlaf.icons.FlatAnimatedIcon; +import com.formdev.flatlaf.util.AnimatedIcon; +import com.formdev.flatlaf.util.ColorFunctions; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatAnimatedIconTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + FlatTestFrame frame = FlatTestFrame.create( args, "FlatAnimatedIconTest" ); + frame.showFrame( FlatAnimatedIconTest::new ); + } ); + } + + FlatAnimatedIconTest() { + initComponents(); + + AnimatedRadioButtonIcon radioIcon = new AnimatedRadioButtonIcon(); + radioButton1.setIcon( radioIcon ); + radioButton2.setIcon( radioIcon ); + radioButton3.setIcon( radioIcon ); + + checkBox1.setIcon( new AnimatedSwitchIcon() ); + checkBox2.setIcon( new AnimatedMinimalTestIcon() ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + radioButton1 = new JRadioButton(); + radioButton2 = new JRadioButton(); + radioButton3 = new JRadioButton(); + checkBox1 = new JCheckBox(); + checkBox2 = new JCheckBox(); + durationLabel = new JLabel(); + durationField = new JSpinner(); + + //======== this ======== + setLayout(new MigLayout( + "insets dialog,hidemode 3", + // columns + "[]para" + + "[fill]", + // rows + "[]" + + "[]" + + "[]para" + + "[]" + + "[]" + + "[grow]" + + "[]")); + + //---- radioButton1 ---- + radioButton1.setText("radio 1"); + radioButton1.setSelected(true); + add(radioButton1, "cell 0 0"); + + //---- radioButton2 ---- + radioButton2.setText("radio 2"); + add(radioButton2, "cell 0 1"); + + //---- radioButton3 ---- + radioButton3.setText("radio 3"); + add(radioButton3, "cell 0 2"); + + //---- checkBox1 ---- + checkBox1.setText("switch"); + add(checkBox1, "cell 0 3"); + + //---- checkBox2 ---- + checkBox2.setText("minimal"); + add(checkBox2, "cell 0 4"); + + //---- durationLabel ---- + durationLabel.setText("Duration:"); + add(durationLabel, "cell 0 6 2 1"); + + //---- durationField ---- + durationField.setModel(new SpinnerNumberModel(200, 100, null, 50)); + add(durationField, "cell 0 6 2 1"); + + //---- buttonGroup1 ---- + ButtonGroup buttonGroup1 = new ButtonGroup(); + buttonGroup1.add(radioButton1); + buttonGroup1.add(radioButton2); + buttonGroup1.add(radioButton3); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JRadioButton radioButton1; + private JRadioButton radioButton2; + private JRadioButton radioButton3; + private JCheckBox checkBox1; + private JCheckBox checkBox2; + private JLabel durationLabel; + private JSpinner durationField; + // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class AnimatedRadioButtonIcon -------------------------------------- + + /** + * Experimental radio button icon that: + * - fades icon color from off to on color + * - animates size of center dot from zero to full size + */ + private class AnimatedRadioButtonIcon + extends FlatAnimatedIcon + { + private static final int SIZE = 16; + private static final int BORDER_SIZE = 2; + private static final int DOT_SIZE = 8; + + private final Color offColor = Color.lightGray; + private final Color onColor = Color.red; + + public AnimatedRadioButtonIcon() { + super( SIZE, SIZE, null ); + } + + @Override + public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + Color color = ColorFunctions.mix( onColor, offColor, animatedValue );; + + // border + g.setColor( color ); + g.fillOval( 0, 0, SIZE, SIZE ); + + // background + g.setColor( c.getBackground() ); + int bwh = SIZE - (BORDER_SIZE * 2); + g.fillOval( BORDER_SIZE, BORDER_SIZE, bwh, bwh ); + + // dot + float dotDiameter = DOT_SIZE * animatedValue; + float xy = (SIZE - dotDiameter) / 2f; + g.setColor( color ); + ((Graphics2D)g).fill( new Ellipse2D.Float( xy, xy, dotDiameter, dotDiameter ) ); + } + + @Override + public float getValue( Component c ) { + return ((JRadioButton)c).isSelected() ? 1 : 0; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } + + //---- class AnimatedSwitchIcon ------------------------------------------- + + public class AnimatedSwitchIcon + extends FlatAnimatedIcon + { + private final Color offColor = Color.lightGray; + private final Color onColor = Color.red; + + public AnimatedSwitchIcon() { + super( 28, 16, null ); + } + + @Override + public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + Color color = ColorFunctions.mix( onColor, offColor, animatedValue );; + + g.setColor( color ); + g.fillRoundRect( x, y, width, height, height, height ); + + int thumbSize = height - 4; + int thumbX = x + 2 + Math.round( (width - 4 - thumbSize) * animatedValue ); + int thumbY = y + 2; + g.setColor( Color.white ); + g.fillOval( thumbX, thumbY, thumbSize, thumbSize ); + } + + @Override + public float getValue( Component c ) { + return ((AbstractButton)c).isSelected() ? 1 : 0; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } + + //---- class AnimatedMinimalTestIcon -------------------------------------- + + /** + * Minimal example for an animated icon. + */ + private class AnimatedMinimalTestIcon + implements AnimatedIcon + { + @Override + public int getIconWidth() { + return 100; + } + + @Override + public int getIconHeight() { + return 20; + } + + @Override + public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + int w = getIconWidth(); + int h = getIconHeight(); + + g.setColor( Color.red ); + g.drawRect( x, y, w - 1, h - 1 ); + g.fillRect( x, y, Math.round( w * animatedValue ), h ); + } + + @Override + public float getValue( Component c ) { + return ((AbstractButton)c).isSelected() ? 1 : 0; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd new file mode 100644 index 00000000..11c55e81 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd @@ -0,0 +1,72 @@ +JFDML JFormDesigner: "7.0.2.0.298" Java: "15" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets dialog,hidemode 3" + "$columnConstraints": "[]para[fill]" + "$rowConstraints": "[][][]para[][][grow][]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "radioButton1" + "text": "radio 1" + "$buttonGroup": new FormReference( "buttonGroup1" ) + "selected": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "radioButton2" + "text": "radio 2" + "$buttonGroup": new FormReference( "buttonGroup1" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "radioButton3" + "text": "radio 3" + "$buttonGroup": new FormReference( "buttonGroup1" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "checkBox1" + "text": "switch" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "checkBox2" + "text": "minimal" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "durationLabel" + "text": "Duration:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6 2 1" + } ) + add( new FormComponent( "javax.swing.JSpinner" ) { + name: "durationField" + "model": new javax.swing.SpinnerNumberModel { + minimum: 100 + stepSize: 50 + value: 200 + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6 2 1" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 415, 350 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "buttonGroup1" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 360 ) + } ) + } +}