1 /*
   2  * Copyright (c) 2010, 2015, 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  * A high-level effect that renders a shadow of the given content behind
  45  * the content with the specified color, radius, and offset.
  46  *
  47  * <p>
  48  * Example:
  49  * <pre><code>
  50 
  51  * DropShadow dropShadow = new DropShadow();
  52  * dropShadow.setRadius(5.0);
  53  * dropShadow.setOffsetX(3.0);
  54  * dropShadow.setOffsetY(3.0);
  55  * dropShadow.setColor(Color.color(0.4, 0.5, 0.5));  *
  56  *
  57  * Text text = new Text();
  58  * text.setEffect(dropShadow);
  59  * text.setCache(true);
  60  * text.setX(10.0);
  61  * text.setY(70.0);
  62  * text.setFill(Color.web("0x3b596d"));
  63  * text.setText("JavaFX drop shadow...");
  64  * text.setFont(Font.font(null, FontWeight.BOLD, 40));
  65  *
  66  * DropShadow dropShadow2 = new DropShadow();
  67  * dropShadow2.setOffsetX(6.0);
  68  * dropShadow2.setOffsetY(4.0);
  69  *
  70  * Circle circle = new Circle();
  71  * circle.setEffect(dropShadow2);
  72  * circle.setCenterX(50.0);
  73  * circle.setCenterY(125.0);
  74  * circle.setRadius(30.0);
  75  * circle.setFill(Color.STEELBLUE);
  76  * circle.setCache(true);
  77  * </pre></code>
  78  *
  79  * <p>
  80  * The code above produces the following:
  81  * </p>
  82  * <p>
  83  * <img src="doc-files/dropshadow.png"/>
  84  * </p>
  85  * @since JavaFX 2.0
  86  */
  87 public class DropShadow extends Effect {
  88     private boolean changeIsLocal;
  89 
  90     /**
  91      * Creates a new instance of DropShadow with default parameters.
  92      */
  93     public DropShadow() {}
  94 
  95     /**
  96      * Creates a new instance of DropShadow with specified radius and color.
  97      * @param radius the radius of the shadow blur kernel
  98      * @param color the shadow {@code Color}
  99      * @since JavaFX 2.1
 100      */
 101     public DropShadow(double radius, Color color) {
 102         setRadius(radius);
 103         setColor(color);
 104     }
 105 
 106     /**
 107      * Creates a new instance of DropShadow with the specified radius, offsetX,
 108      * offsetY and color.
 109      * @param radius the radius of the shadow blur kernel
 110      * @param offsetX the shadow offset in the x direction
 111      * @param offsetY the shadow offset in the y direction
 112      * @param color the shadow {@code Color}
 113      * @since JavaFX 2.1
 114      */
 115     public DropShadow(double radius, double offsetX, double offsetY, Color color) {
 116         setRadius(radius);
 117         setOffsetX(offsetX);
 118         setOffsetY(offsetY);
 119         setColor(color);
 120     }
 121 
 122     /**
 123      * Creates a new instance of DropShadow with the specified blurType, color,
 124      * radius, spread, offsetX and offsetY.
 125      * @param blurType the algorithm used to blur the shadow
 126      * @param color the shadow {@code Color}
 127      * @param radius the radius of the shadow blur kernel
 128      * @param spread the portion of the radius where the contribution of
 129      * the source material will be 100%
 130      * @param offsetX the shadow offset in the x direction
 131      * @param offsetY the shadow offset in the y direction
 132      * @since JavaFX 2.1
 133      */
 134     public DropShadow(BlurType blurType, Color color, double radius, double spread,
 135             double offsetX, double offsetY) {
 136         setBlurType(blurType);
 137         setColor(color);
 138         setRadius(radius);
 139         setSpread(spread);
 140         setOffsetX(offsetX);
 141         setOffsetY(offsetY);
 142     }
 143 
 144     @Override
 145     com.sun.scenario.effect.DropShadow impl_createImpl() {
 146         return new com.sun.scenario.effect.DropShadow();
 147     };
 148     /**
 149      * The input for this {@code Effect}.
 150      * If set to {@code null}, or left unspecified, a graphical image of
 151      * the {@code Node} to which the {@code Effect} is attached will be
 152      * used as the input.
 153      * @defaultValue null
 154      */
 155     private ObjectProperty<Effect> input;
 156 
 157 
 158     public final void setInput(Effect value) {
 159         inputProperty().set(value);
 160     }
 161 
 162     public final Effect getInput() {
 163         return input == null ? null : input.get();
 164     }
 165 
 166     public final ObjectProperty<Effect> inputProperty() {
 167         if (input == null) {
 168             input = new EffectInputProperty("input");
 169         }
 170         return input;
 171     }
 172 
 173     @Override
 174     boolean impl_checkChainContains(Effect e) {
 175         Effect localInput = getInput();
 176         if (localInput == null)
 177             return false;
 178         if (localInput == e)
 179             return true;
 180         return localInput.impl_checkChainContains(e);
 181     }
 182 
 183     /**
 184      * The radius of the shadow blur kernel.
 185      * This attribute controls the distance that the shadow is spread
 186      * to each side of the source pixels.
 187      * Setting the radius is equivalent to setting both the {@code width}
 188      * and {@code height} attributes to a value of {@code (2 * radius + 1)}.
 189      * <pre>
 190      *       Min:   0.0
 191      *       Max: 127.0
 192      *   Default:  10.0
 193      *  Identity:   0.0
 194      * </pre>
 195      * @defaultValue 10.0
 196      */
 197     private DoubleProperty radius;
 198 
 199 
 200     public final void setRadius(double value) {
 201         radiusProperty().set(value);
 202     }
 203 
 204     public final double getRadius() {
 205         return radius == null ? 10 : radius.get();
 206     }
 207 
 208     public final DoubleProperty radiusProperty() {
 209         if (radius == null) {
 210             radius = new DoublePropertyBase(10) {
 211 
 212                 @Override
 213                 public void invalidated() {
 214                     // gettter here is necessary to make the property valid
 215                     double localRadius = getRadius();
 216                     if (!changeIsLocal) {
 217                         changeIsLocal = true;
 218                         updateRadius(localRadius);
 219                         changeIsLocal = false;
 220                         markDirty(EffectDirtyBits.EFFECT_DIRTY);
 221                         effectBoundsChanged();
 222                     }
 223                 }
 224 
 225                 @Override
 226                 public Object getBean() {
 227                     return DropShadow.this;
 228                 }
 229 
 230                 @Override
 231                 public String getName() {
 232                     return "radius";
 233                 }
 234             };
 235         }
 236         return radius;
 237     }
 238 
 239     private void updateRadius(double value) {
 240         double newdim = (value * 2 + 1);
 241         if (width != null && width.isBound()) {
 242             // if neither is readonly we would set both width and
 243             // height to radius*2+1 (i.e. newdim), but if one of
 244             // them is bound then we need to set the other to a
 245             // value that would map back to the radius we have been
 246             // given.
 247             // To do that we equate the average of the two values
 248             // to newdim, the value we want them both to have, and
 249             // then solve for the missing value:
 250             // avg(w,h) == radius * 2 + 1
 251             // (w+h)/2 == newdim
 252             // w+h == newdim * 2
 253             // h = newdim * 2 - w
 254             // w = newdim * 2 - h
 255             if (height == null || !height.isBound()) {
 256                 setHeight(newdim * 2 - getWidth());
 257             }
 258         } else if (height != null && height.isBound()) {
 259             setWidth(newdim * 2 - getHeight());
 260         } else {
 261             setWidth(newdim);
 262             setHeight(newdim);
 263         }
 264     }
 265 
 266     /**
 267      * The horizontal size of the shadow blur kernel.
 268      * This attribute controls the horizontal size of the total area over
 269      * which the shadow of a single pixel is distributed by the blur algorithm.
 270      * Values less than {@code 1.0} are not distributed beyond the original
 271      * pixel and so have no blurring effect on the shadow.
 272      * <pre>
 273      *       Min:   0.0
 274      *       Max: 255.0
 275      *   Default:  21.0
 276      *  Identity:  &lt;1.0
 277      * </pre>
 278      * @defaultValue 21.0
 279      */
 280     private DoubleProperty width;
 281 
 282 
 283     public final void setWidth(double value) {
 284         widthProperty().set(value);
 285     }
 286 
 287     public final double getWidth() {
 288         return width == null ? 21 : width.get();
 289     }
 290 
 291     public final DoubleProperty widthProperty() {
 292         if (width == null) {
 293             width = new DoublePropertyBase(21) {
 294 
 295                 @Override
 296                 public void invalidated() {
 297                     // gettter here is necessary to make the property valid
 298                     double localWidth = getWidth();
 299                     if (!changeIsLocal) {
 300                         changeIsLocal = true;
 301                         updateWidth(localWidth);
 302                         changeIsLocal = false;
 303                         markDirty(EffectDirtyBits.EFFECT_DIRTY);
 304                         effectBoundsChanged();
 305                     }
 306                 }
 307 
 308                 @Override
 309                 public Object getBean() {
 310                     return DropShadow.this;
 311                 }
 312 
 313                 @Override
 314                 public String getName() {
 315                     return "width";
 316                 }
 317             };
 318         }
 319         return width;
 320     }
 321 
 322     private void updateWidth(double value) {
 323         if (radius == null || !radius.isBound()) {
 324             double newrad = ((value + getHeight()) / 2);
 325             newrad = ((newrad - 1) / 2);
 326             if (newrad < 0) {
 327                 newrad = 0;
 328             }
 329             setRadius(newrad);
 330         } else {
 331             if (height == null || !height.isBound()) {
 332                 double newdim = (getRadius() * 2 + 1);
 333                 setHeight(newdim * 2 - value);
 334             }
 335         }
 336     }
 337 
 338     /**
 339      * The vertical size of the shadow blur kernel.
 340      * This attribute controls the vertical size of the total area over
 341      * which the shadow of a single pixel is distributed by the blur algorithm.
 342      * Values less than {@code 1.0} are not distributed beyond the original
 343      * pixel and so have no blurring effect on the shadow.
 344      * <pre>
 345      *       Min:   0.0
 346      *       Max: 255.0
 347      *   Default:  21.0
 348      *  Identity:  &lt;1.0
 349      * </pre>
 350      * @defaultValue 21.0
 351      */
 352     private DoubleProperty height;
 353 
 354 
 355     public final void setHeight(double value) {
 356         heightProperty().set(value);
 357     }
 358 
 359     public final double getHeight() {
 360         return height == null ? 21 : height.get();
 361     }
 362 
 363     public final DoubleProperty heightProperty() {
 364         if (height == null) {
 365             height = new DoublePropertyBase(21) {
 366 
 367                 @Override
 368                 public void invalidated() {
 369                     // gettter here is necessary to make the property valid
 370                     double localHeight = getHeight();
 371                     if (!changeIsLocal) {
 372                         changeIsLocal = true;
 373                         updateHeight(localHeight);
 374                         changeIsLocal = false;
 375                         markDirty(EffectDirtyBits.EFFECT_DIRTY);
 376                         effectBoundsChanged();
 377                     }
 378                 }
 379 
 380                 @Override
 381                 public Object getBean() {
 382                     return DropShadow.this;
 383                 }
 384 
 385                 @Override
 386                 public String getName() {
 387                     return "height";
 388                 }
 389             };
 390         }
 391         return height;
 392     }
 393 
 394     private void updateHeight(double value) {
 395         if (radius == null || !radius.isBound()) {
 396             double newrad = ((getWidth() + value) / 2);
 397             newrad = ((newrad - 1) / 2);
 398             if (newrad < 0) {
 399                 newrad = 0;
 400             }
 401             setRadius(newrad);
 402         } else {
 403             if (width == null || !width.isBound()) {
 404                 double newdim = (getRadius() * 2 + 1);
 405                 setWidth(newdim * 2 - value);
 406             }
 407         }
 408     }
 409 
 410     /**
 411      * The algorithm used to blur the shadow.
 412      * <pre>
 413      *       Min: n/a
 414      *       Max: n/a
 415      *   Default: BlurType.THREE_PASS_BOX
 416      *  Identity: n/a
 417      * </pre>
 418      * @defaultValue THREE_PASS_BOX
 419      */
 420     private ObjectProperty<BlurType> blurType;
 421 
 422 
 423     public final void setBlurType(BlurType value) {
 424         blurTypeProperty().set(value);
 425     }
 426 
 427     public final BlurType getBlurType() {
 428         return blurType == null ? BlurType.THREE_PASS_BOX : blurType.get();
 429     }
 430 
 431     public final ObjectProperty<BlurType> blurTypeProperty() {
 432         if (blurType == null) {
 433             blurType = new ObjectPropertyBase<BlurType>(BlurType.THREE_PASS_BOX) {
 434 
 435                 @Override
 436                 public void invalidated() {
 437                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 438                     effectBoundsChanged();
 439                 }
 440 
 441                 @Override
 442                 public Object getBean() {
 443                     return DropShadow.this;
 444                 }
 445 
 446                 @Override
 447                 public String getName() {
 448                     return "blurType";
 449                 }
 450             };
 451         }
 452         return blurType;
 453     }
 454 
 455     /**
 456      * The spread of the shadow.
 457      * The spread is the portion of the radius where the contribution of
 458      * the source material will be 100%.
 459      * The remaining portion of the radius will have a contribution
 460      * controlled by the blur kernel.
 461      * A spread of {@code 0.0} will result in a distribution of the
 462      * shadow determined entirely by the blur algorithm.
 463      * A spread of {@code 1.0} will result in a solid growth outward of the
 464      * source material opacity to the limit of the radius with a very sharp
 465      * cutoff to transparency at the radius.
 466      * <pre>
 467      *       Min: 0.0
 468      *       Max: 1.0
 469      *   Default: 0.0
 470      *  Identity: 0.0
 471      * </pre>
 472      * @defaultValue 0.0
 473      */
 474     private DoubleProperty spread;
 475 
 476 
 477     public final void setSpread(double value) {
 478         spreadProperty().set(value);
 479     }
 480 
 481     public final double getSpread() {
 482         return spread == null ? 0.0 : spread.get();
 483     }
 484 
 485     public final DoubleProperty spreadProperty() {
 486         if (spread == null) {
 487             spread = new DoublePropertyBase() {
 488 
 489                 @Override
 490                 public void invalidated() {
 491                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 492                 }
 493 
 494                 @Override
 495                 public Object getBean() {
 496                     return DropShadow.this;
 497                 }
 498 
 499                 @Override
 500                 public String getName() {
 501                     return "spread";
 502                 }
 503             };
 504         }
 505         return spread;
 506     }
 507 
 508     /**
 509      * The shadow {@code Color}.
 510      * <pre>
 511      *       Min: n/a
 512      *       Max: n/a
 513      *   Default: Color.BLACK
 514      *  Identity: n/a
 515      * </pre>
 516      * @defaultValue BLACK
 517      */
 518     private ObjectProperty<Color> color;
 519 
 520 
 521     public final void setColor(Color value) {
 522         colorProperty().set(value);
 523     }
 524 
 525     public final Color getColor() {
 526         return color == null ? Color.BLACK : color.get();
 527     }
 528 
 529     public final ObjectProperty<Color> colorProperty() {
 530         if (color == null) {
 531             color = new ObjectPropertyBase<Color>(Color.BLACK) {
 532 
 533                 @Override
 534                 public void invalidated() {
 535                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 536                 }
 537 
 538                 @Override
 539                 public Object getBean() {
 540                     return DropShadow.this;
 541                 }
 542 
 543                 @Override
 544                 public String getName() {
 545                     return "color";
 546                 }
 547             };
 548         }
 549         return color;
 550     }
 551 
 552     /**
 553      * The shadow offset in the x direction, in pixels.
 554      * <pre>
 555      *       Min: n/a
 556      *       Max: n/a
 557      *   Default: 0.0
 558      *  Identity: 0.0
 559      * </pre>
 560      * @defaultValue 0.0
 561      */
 562     private DoubleProperty offsetX;
 563 
 564     public final void setOffsetX(double value) {
 565         offsetXProperty().set(value);
 566 
 567     }
 568 
 569     public final double getOffsetX() {
 570         return offsetX == null ? 0 : offsetX.get();
 571     }
 572 
 573     public final DoubleProperty offsetXProperty() {
 574         if (offsetX == null) {
 575             offsetX = new DoublePropertyBase() {
 576 
 577                 @Override
 578                 public void invalidated() {
 579                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 580                     effectBoundsChanged();
 581                 }
 582 
 583                 @Override
 584                 public Object getBean() {
 585                     return DropShadow.this;
 586                 }
 587 
 588                 @Override
 589                 public String getName() {
 590                     return "offsetX";
 591                 }
 592             };
 593         }
 594         return offsetX;
 595     }
 596 
 597     /**
 598      * The shadow offset in the y direction, in pixels.
 599      * <pre>
 600      *       Min: n/a
 601      *       Max: n/a
 602      *   Default: 0.0
 603      *  Identity: 0.0
 604      * </pre>
 605      * @defaultValue 0.0
 606      */
 607     private DoubleProperty offsetY;
 608 
 609 
 610     public final void setOffsetY(double value) {
 611         offsetYProperty().set(value);
 612     }
 613 
 614     public final double getOffsetY() {
 615         return offsetY == null ? 0 : offsetY.get();
 616     }
 617 
 618     public final DoubleProperty offsetYProperty() {
 619         if (offsetY == null) {
 620             offsetY = new DoublePropertyBase() {
 621 
 622                 @Override
 623                 public void invalidated() {
 624                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 625                     effectBoundsChanged();
 626                 }
 627 
 628                 @Override
 629                 public Object getBean() {
 630                     return DropShadow.this;
 631                 }
 632 
 633                 @Override
 634                 public String getName() {
 635                     return "offsetY";
 636                 }
 637             };
 638         }
 639         return offsetY;
 640     }
 641 
 642     private float getClampedWidth() {
 643         return (float) Utils.clamp(0, getWidth(), 255);
 644     }
 645 
 646     private float getClampedHeight() {
 647         return (float) Utils.clamp(0, getHeight(), 255);
 648     }
 649 
 650     private float getClampedSpread() {
 651         return (float) Utils.clamp(0, getSpread(), 1);
 652     }
 653 
 654     private Color getColorInternal() {
 655         Color c = getColor();
 656         return c == null ? Color.BLACK : c;
 657     }
 658 
 659     private BlurType getBlurTypeInternal() {
 660         BlurType bt = getBlurType();
 661         return bt == null ? BlurType.THREE_PASS_BOX : bt;
 662     }
 663 
 664     @Override
 665     void impl_update() {
 666         Effect localInput = getInput();
 667         if (localInput != null) {
 668             localInput.impl_sync();
 669         }
 670 
 671         com.sun.scenario.effect.DropShadow peer =
 672                 (com.sun.scenario.effect.DropShadow) impl_getImpl();
 673         peer.setShadowSourceInput(localInput == null ? null : localInput.impl_getImpl());
 674         peer.setContentInput(localInput == null ? null : localInput.impl_getImpl());
 675         peer.setGaussianWidth(getClampedWidth());
 676         peer.setGaussianHeight(getClampedHeight());
 677         peer.setSpread(getClampedSpread());
 678         peer.setShadowMode(Toolkit.getToolkit().toShadowMode(getBlurTypeInternal()));
 679         peer.setColor(Toolkit.getToolkit().toColor4f(getColorInternal()));
 680         peer.setOffsetX((int) getOffsetX());
 681         peer.setOffsetY((int) getOffsetY());
 682     }
 683 
 684     /**
 685      * @treatAsPrivate implementation detail
 686      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 687      */
 688     @Deprecated
 689     @Override
 690     public BaseBounds impl_getBounds(BaseBounds bounds,
 691                                      BaseTransform tx,
 692                                      Node node,
 693                                      BoundsAccessor boundsAccessor) {
 694         bounds = getInputBounds(bounds,
 695                                 BaseTransform.IDENTITY_TRANSFORM,
 696                                 node, boundsAccessor,
 697                                 getInput());
 698 
 699         int shadowX = (int) getOffsetX();
 700         int shadowY = (int) getOffsetY();
 701 
 702         BaseBounds shadowBounds = BaseBounds.getInstance(bounds.getMinX() + shadowX,
 703                                                          bounds.getMinY() + shadowY,
 704                                                          bounds.getMinZ(),
 705                                                          bounds.getMaxX() + shadowX,
 706                                                          bounds.getMaxY() + shadowY,
 707                                                          bounds.getMaxZ());
 708 
 709         shadowBounds = getShadowBounds(shadowBounds, tx,
 710                                        getClampedWidth(),
 711                                        getClampedHeight(),
 712                                        getBlurTypeInternal());
 713         BaseBounds contentBounds = transformBounds(tx, bounds);
 714         BaseBounds ret = contentBounds.deriveWithUnion(shadowBounds);
 715 
 716         return ret;
 717     }
 718 
 719     /**
 720      * @treatAsPrivate implementation detail
 721      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 722      */
 723     @Deprecated
 724     @Override
 725     public Effect impl_copy() {
 726         DropShadow d = new DropShadow(this.getBlurType(), this.getColor(),
 727                 this.getRadius(), this.getSpread(), this.getOffsetX(),
 728                 this.getOffsetY());
 729         d.setInput(this.getInput());
 730         d.setWidth(this.getWidth());
 731         d.setHeight(this.getHeight());
 732         return d;
 733     }
 734 }