1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.effect;
  27 
  28 import javafx.beans.property.DoubleProperty;
  29 import javafx.beans.property.DoublePropertyBase;
  30 import javafx.beans.property.ObjectProperty;
  31 import javafx.beans.property.ObjectPropertyBase;
  32 import javafx.scene.Node;
  33 import javafx.scene.paint.Color;
  34 
  35 import com.sun.javafx.util.Utils;
  36 import com.sun.javafx.effect.EffectDirtyBits;
  37 import com.sun.javafx.geom.BaseBounds;
  38 import com.sun.javafx.geom.transform.BaseTransform;
  39 import com.sun.javafx.scene.BoundsAccessor;
  40 import com.sun.javafx.tk.Toolkit;
  41 
  42 
  43 /**
  44  * An effect which creates a monochrome duplicate of an input with
  45  * blurry edges.
  46  * This effect is primarily used along with its default black color for
  47  * purposes of combining it with the original to create a shadow.
  48  * It can also be used with a light color and combined with an original
  49  * to create a glow effect.
  50  * The {@link DropShadow} effect is a utility effect which automatically
  51  * combines this {@code Shadow} effect with an original graphic for ease
  52  * of adding a shadow to an existing scene graph {@code Node} with a
  53  * single effect.
  54  * @since JavaFX 2.0
  55  */
  56 public class Shadow extends Effect {
  57     private boolean changeIsLocal;
  58 
  59     /**
  60      * Creates a new instance of Shadow with default parameters.
  61      */
  62     public Shadow() {}
  63 
  64     /**
  65      * Creates a new instance of Shadow with specified radius and color.
  66      * @param radius the radius of the shadow blur kernel
  67      * @param color the shadow {@code Color}
  68      * @since JavaFX 2.1
  69      */
  70     public Shadow(double radius, Color color) {
  71         setRadius(radius);
  72         setColor(color);
  73     }
  74 
  75     /**
  76      * Creates a new instance of Shadow with the specified blurType, color,
  77      * radius.
  78      * @param blurType the algorithm used to blur the shadow
  79      * @param color the shadow {@code Color}
  80      * @param radius the radius of the shadow blur kernel
  81      * @since JavaFX 2.1
  82      */
  83     public Shadow(BlurType blurType, Color color, double radius) {
  84         setBlurType(blurType);
  85         setColor(color);
  86         setRadius(radius);
  87     }
  88 
  89     @Override
  90     com.sun.scenario.effect.GeneralShadow impl_createImpl() {
  91         return new com.sun.scenario.effect.GeneralShadow();
  92     };
  93     /**
  94      * The input for this {@code Effect}.
  95      * If set to {@code null}, or left unspecified, a graphical image of
  96      * the {@code Node} to which the {@code Effect} is attached will be
  97      * used as the input.
  98      * @defaultValue null
  99      */
 100     private ObjectProperty<Effect> input;
 101 
 102 
 103     public final void setInput(Effect value) {
 104         inputProperty().set(value);
 105     }
 106 
 107     public final Effect getInput() {
 108         return input == null ? null : input.get();
 109     }
 110 
 111     public final ObjectProperty<Effect> inputProperty() {
 112         if (input == null) {
 113             input = new EffectInputProperty("input");
 114         }
 115         return input;
 116     }
 117 
 118     @Override
 119     boolean impl_checkChainContains(Effect e) {
 120         Effect localInput = getInput();
 121         if (localInput == null)
 122             return false;
 123         if (localInput == e)
 124             return true;
 125         return localInput.impl_checkChainContains(e);
 126     }
 127 
 128     /**
 129      * The radius of the shadow blur kernel.
 130      * This attribute controls the distance that the shadow is spread
 131      * to each side of the source pixels.
 132      * Setting the radius is equivalent to setting both the {@code width}
 133      * and {@code height} attributes to a value of {@code (2 * radius + 1)}.
 134      * <pre>
 135      *       Min:   0.0
 136      *       Max: 127.0
 137      *   Default:  10.0
 138      *  Identity:   0.0
 139      * </pre>
 140      * @defaultValue 10.0
 141      */
 142     private DoubleProperty radius;
 143 
 144 
 145     public final void setRadius(double value) {
 146         radiusProperty().set(value);
 147     }
 148 
 149     public final double getRadius() {
 150         return radius == null ? 10 : radius.get();
 151     }
 152 
 153     public final DoubleProperty radiusProperty() {
 154         if (radius == null) {
 155             radius = new DoublePropertyBase(10) {
 156 
 157                 @Override
 158                 public void invalidated() {
 159                     // gettter here is necessary to make the property valid
 160                     double localRadius = getRadius();
 161                     if (!changeIsLocal) {
 162                         changeIsLocal = true;
 163                         updateRadius(localRadius);
 164                         changeIsLocal = false;
 165                         markDirty(EffectDirtyBits.EFFECT_DIRTY);
 166                         effectBoundsChanged();
 167                     }
 168                 }
 169 
 170                 @Override
 171                 public Object getBean() {
 172                     return Shadow.this;
 173                 }
 174 
 175                 @Override
 176                 public String getName() {
 177                     return "radius";
 178                 }
 179             };
 180         }
 181         return radius;
 182     }
 183 
 184     private void updateRadius(double value) {
 185         double newdim = (value * 2 + 1);
 186         if (width != null && width.isBound()) {
 187             if (height == null || !height.isBound()) {
 188                 setHeight(newdim * 2 - getWidth());
 189             }
 190         } else if (height != null && height.isBound()) {
 191             setWidth(newdim * 2 - getHeight());
 192         } else {
 193             setWidth(newdim);
 194             setHeight(newdim);
 195         }
 196     }
 197 
 198     /**
 199      * The horizontal size of the shadow blur kernel.
 200      * This attribute controls the horizontal size of the total area over
 201      * which the shadow of a single pixel is distributed by the blur algorithm.
 202      * Values less than {@code 1.0} are not distributed beyond the original
 203      * pixel and so have no blurring effect on the shadow.
 204      * <pre>
 205      *       Min:   0.0
 206      *       Max: 255.0
 207      *   Default:  21.0
 208      *  Identity:  &lt;1.0
 209      * </pre>
 210      * @defaultValue 21.0
 211      */
 212     private DoubleProperty width;
 213 
 214 
 215     public final void setWidth(double value) {
 216         widthProperty().set(value);
 217     }
 218 
 219     public final double getWidth() {
 220         return width == null ? 21 : width.get();
 221     }
 222 
 223     public final DoubleProperty widthProperty() {
 224         if (width == null) {
 225             width = new DoublePropertyBase(21) {
 226 
 227                 @Override
 228                 public void invalidated() {
 229                     // gettter here is necessary to make the property valid
 230                     double localWidth = getWidth();
 231                     if (!changeIsLocal) {
 232                         changeIsLocal = true;
 233                         updateWidth(localWidth);
 234                         changeIsLocal = false;
 235                         markDirty(EffectDirtyBits.EFFECT_DIRTY);
 236                         effectBoundsChanged();
 237                     }
 238                 }
 239 
 240                 @Override
 241                 public Object getBean() {
 242                     return Shadow.this;
 243                 }
 244 
 245                 @Override
 246                 public String getName() {
 247                     return "width";
 248                 }
 249             };
 250         }
 251         return width;
 252     }
 253 
 254     private void updateWidth(double value) {
 255         if (radius == null || !radius.isBound()) {
 256             double newrad = ((value + getHeight()) / 2);
 257             newrad = ((newrad - 1) / 2);
 258             if (newrad < 0) {
 259                 newrad = 0;
 260             }
 261             setRadius(newrad);
 262         } else {
 263             // special case when radius is bound
 264             if (height == null || !height.isBound()) {
 265                 double newdim = (getRadius() * 2 + 1);
 266                 setHeight(newdim * 2 - value);
 267             }
 268         }
 269     }
 270 
 271     /**
 272      * The vertical size of the shadow blur kernel.
 273      * This attribute controls the vertical size of the total area over
 274      * which the shadow of a single pixel is distributed by the blur algorithm.
 275      * Values less than {@code 1.0} are not distributed beyond the original
 276      * pixel and so have no blurring effect on the shadow.
 277      * <pre>
 278      *       Min:   0.0
 279      *       Max: 255.0
 280      *   Default:  21.0
 281      *  Identity:  &lt;1.0
 282      * </pre>
 283      * @defaultValue 21.0
 284      */
 285     private DoubleProperty height;
 286 
 287 
 288     public final void setHeight(double value) {
 289         heightProperty().set(value);
 290     }
 291 
 292     public final double getHeight() {
 293         return height == null ? 21 : height.get();
 294     }
 295 
 296     public final DoubleProperty heightProperty() {
 297         if (height == null) {
 298             height = new DoublePropertyBase(21) {
 299 
 300                 @Override
 301                 public void invalidated() {
 302                     // gettter here is necessary to make the property valid
 303                     double localHeight = getHeight();
 304                     if (!changeIsLocal) {
 305                         changeIsLocal = true;
 306                         updateHeight(localHeight);
 307                         changeIsLocal = false;
 308                         markDirty(EffectDirtyBits.EFFECT_DIRTY);
 309                         effectBoundsChanged();
 310                     }
 311                 }
 312 
 313                 @Override
 314                 public Object getBean() {
 315                     return Shadow.this;
 316                 }
 317 
 318                 @Override
 319                 public String getName() {
 320                     return "height";
 321                 }
 322             };
 323         }
 324         return height;
 325     }
 326 
 327     private void updateHeight(double value) {
 328         if (radius == null || !radius.isBound()) {
 329             double newrad = ((getWidth() + value) / 2);
 330             newrad = ((newrad - 1) / 2);
 331             if (newrad < 0) {
 332                 newrad = 0;
 333             }
 334             setRadius(newrad);
 335         } else {
 336             if (width == null || !width.isBound()) {
 337                 double newdim = (getRadius() * 2 + 1);
 338                 setWidth(newdim * 2 - value);
 339             }
 340         }
 341     }
 342 
 343     /**
 344      * The algorithm used to blur the shadow.
 345      * <pre>
 346      *       Min: n/a
 347      *       Max: n/a
 348      *   Default: BlurType.THREE_PASS_BOX
 349      *  Identity: n/a
 350      * </pre>
 351      * @defaultValue THREE_PASS_BOX
 352      */
 353     private ObjectProperty<BlurType> blurType;
 354 
 355 
 356     public final void setBlurType(BlurType value) {
 357         blurTypeProperty().set(value);
 358     }
 359 
 360     public final BlurType getBlurType() {
 361         return blurType == null ? BlurType.THREE_PASS_BOX : blurType.get();
 362     }
 363 
 364     public final ObjectProperty<BlurType> blurTypeProperty() {
 365         if (blurType == null) {
 366             blurType = new ObjectPropertyBase<BlurType>(BlurType.THREE_PASS_BOX) {
 367 
 368                 @Override
 369                 public void invalidated() {
 370                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 371                     effectBoundsChanged();
 372                 }
 373 
 374                 @Override
 375                 public Object getBean() {
 376                     return Shadow.this;
 377                 }
 378 
 379                 @Override
 380                 public String getName() {
 381                     return "blurType";
 382                 }
 383             };
 384         }
 385         return blurType;
 386     }
 387 
 388     /**
 389      * The shadow {@code Color}.
 390      * <pre>
 391      *       Min: n/a
 392      *       Max: n/a
 393      *   Default: Color.BLACK
 394      *  Identity: n/a
 395      * </pre>
 396      * @defaultValue BLACK
 397      */
 398     private ObjectProperty<Color> color;
 399 
 400 
 401     public final void setColor(Color value) {
 402         colorProperty().set(value);
 403     }
 404 
 405     public final Color getColor() {
 406         return color == null ? Color.BLACK : color.get();
 407     }
 408 
 409     public final ObjectProperty<Color> colorProperty() {
 410         if (color == null) {
 411             color = new ObjectPropertyBase<Color>(Color.BLACK) {
 412 
 413                 @Override
 414                 public void invalidated() {
 415                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 416                 }
 417 
 418                 @Override
 419                 public Object getBean() {
 420                     return Shadow.this;
 421                 }
 422 
 423                 @Override
 424                 public String getName() {
 425                     return "color";
 426                 }
 427             };
 428         }
 429         return color;
 430     }
 431 
 432     private float getClampedWidth() {
 433         return (float) Utils.clamp(0, getWidth(), 255);
 434     }
 435 
 436     private float getClampedHeight() {
 437         return (float) Utils.clamp(0, getHeight(), 255);
 438     }
 439 
 440     private Color getColorInternal() {
 441         Color c = getColor();
 442         return c == null ? Color.BLACK : c;
 443     }
 444 
 445     private BlurType getBlurTypeInternal() {
 446         BlurType bt = getBlurType();
 447         return bt == null ? BlurType.THREE_PASS_BOX : bt;
 448     }
 449 
 450     @Override
 451     void impl_update() {
 452         Effect localInput = getInput();
 453         if (localInput != null) {
 454             localInput.impl_sync();
 455         }
 456 
 457         com.sun.scenario.effect.GeneralShadow peer =
 458                 (com.sun.scenario.effect.GeneralShadow) impl_getImpl();
 459         peer.setInput(localInput == null ? null : localInput.impl_getImpl());
 460         peer.setGaussianWidth(getClampedWidth());
 461         peer.setGaussianHeight(getClampedHeight());
 462         peer.setShadowMode(Toolkit.getToolkit().toShadowMode(getBlurTypeInternal()));
 463         peer.setColor(Toolkit.getToolkit().toColor4f(getColorInternal()));
 464     }
 465 
 466     /**
 467      * @treatAsPrivate implementation detail
 468      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 469      */
 470     @Deprecated
 471     @Override
 472     public BaseBounds impl_getBounds(BaseBounds bounds,
 473                                      BaseTransform tx,
 474                                      Node node,
 475                                      BoundsAccessor boundsAccessor) {
 476         bounds = getInputBounds(bounds,
 477                                 BaseTransform.IDENTITY_TRANSFORM,
 478                                 node, boundsAccessor,
 479                                 getInput());
 480         return getShadowBounds(bounds, tx,
 481                                getClampedWidth(),
 482                                getClampedHeight(),
 483                                getBlurTypeInternal());
 484     }
 485 
 486     /**
 487      * @treatAsPrivate implementation detail
 488      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 489      */
 490     @Deprecated
 491     @Override
 492     public Effect impl_copy() {
 493         Shadow shadow = new Shadow(this.getBlurType(), this.getColor(), this.getRadius());
 494         shadow.setInput(this.getInput());
 495         shadow.setHeight(this.getHeight());
 496         shadow.setWidth(this.getWidth());
 497         return shadow;
 498     }
 499 }