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: <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: <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 }