1 /*
   2  * Copyright (c) 2010, 2016, 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.Observable;
  29 import javafx.beans.property.IntegerProperty;
  30 import javafx.beans.property.ObjectPropertyBase;
  31 import javafx.beans.property.SimpleIntegerProperty;
  32 import javafx.scene.Node;
  33 
  34 import com.sun.javafx.effect.EffectDirtyBits;
  35 import com.sun.javafx.geom.BaseBounds;
  36 import com.sun.javafx.geom.RectBounds;
  37 import com.sun.javafx.geom.transform.BaseTransform;
  38 import com.sun.javafx.scene.BoundsAccessor;
  39 import com.sun.scenario.effect.EffectHelper;
  40 
  41 /**
  42  * The abstract base class for all effect implementations.
  43  * An effect is a graphical algorithm that produces an image, typically
  44  * as a modification of a source image.
  45  * An effect can be associated with a scene graph {@code Node} by setting
  46  * the {@link javafx.scene.Node#effectProperty Node.effect} attribute.
  47  * Some effects change the color properties of the source pixels
  48  * (such as {@link ColorAdjust}),
  49  * others combine multiple images together (such as {@link Blend}),
  50  * while still others warp or move the pixels of the source image around
  51  * (such as {@link DisplacementMap} or {@link PerspectiveTransform}).
  52  * All effects have at least one input defined and the input can be set
  53  * to another effect to chain the effects together and combine their
  54  * results, or it can be left unspecified in which case the effect will
  55  * operate on a graphical rendering of the node it is attached to.
  56  * <p>
  57  * Note: this is a conditional feature. See
  58  * {@link javafx.application.ConditionalFeature#EFFECT ConditionalFeature.EFFECT}
  59  * for more information.
  60  * @since JavaFX 2.0
  61  */
  62 public abstract class Effect {
  63     static {
  64         // This is used by classes in different packages to get access to
  65         // private and package private methods.
  66         EffectHelper.setEffectAccessor(new EffectHelper.EffectAccessor() {
  67 
  68             @Override
  69             public com.sun.scenario.effect.Effect getPeer(Effect effect) {
  70                 return effect.getPeer();
  71             }
  72 
  73             @Override
  74             public void sync(Effect effect) {
  75                 effect.sync();
  76             }
  77 
  78             @Override
  79             public IntegerProperty effectDirtyProperty(Effect effect) {
  80                 return effect.effectDirtyProperty();
  81             }
  82 
  83             @Override
  84             public boolean isEffectDirty(Effect effect) {
  85                 return effect.isEffectDirty();
  86             }
  87 
  88             @Override
  89             public BaseBounds getBounds(Effect effect, BaseBounds bounds,
  90                     BaseTransform tx, Node node, BoundsAccessor boundsAccessor) {
  91                 return effect.getBounds(bounds, tx, node, boundsAccessor);
  92             }
  93 
  94             @Override
  95             public Effect copy(Effect effect) {
  96                 return effect.copy();
  97             }
  98 
  99             @Override
 100             public com.sun.scenario.effect.Blend.Mode getToolkitBlendMode(BlendMode mode) {
 101                 return Blend.getToolkitMode(mode);
 102             }
 103         });
 104     }
 105 
 106     /**
 107      * Creates a new Effect.
 108      */
 109     protected Effect() {
 110        markDirty(EffectDirtyBits.EFFECT_DIRTY);
 111     }
 112 
 113     void effectBoundsChanged() {
 114         toggleDirty(EffectDirtyBits.BOUNDS_CHANGED);
 115     }
 116 
 117     private com.sun.scenario.effect.Effect peer;
 118     abstract com.sun.scenario.effect.Effect createPeer();
 119 
 120     com.sun.scenario.effect.Effect getPeer() {
 121         if (peer == null) {
 122             peer = createPeer();
 123         }
 124         return peer;
 125     }
 126 
 127     // effect is marked dirty in the constructor, so we don't need to be lazy here
 128     private IntegerProperty effectDirty =
 129             new SimpleIntegerProperty(this, "effectDirty");
 130 
 131     private void setEffectDirty(int value) {
 132         effectDirtyProperty().set(value);
 133     }
 134 
 135     private final IntegerProperty effectDirtyProperty() {
 136         return effectDirty;
 137     }
 138 
 139     private final boolean isEffectDirty() {
 140         return isEffectDirty(EffectDirtyBits.EFFECT_DIRTY);
 141     }
 142 
 143     /**
 144      * Set the specified dirty bit
 145      */
 146     final void markDirty(EffectDirtyBits dirtyBit) {
 147         setEffectDirty(effectDirty.get() | dirtyBit.getMask());
 148     }
 149 
 150     /**
 151      * Toggle the specified dirty bit
 152      */
 153     private void toggleDirty(EffectDirtyBits dirtyBit) {
 154         setEffectDirty(effectDirty.get() ^ dirtyBit.getMask());
 155     }
 156 
 157     /**
 158      * Test the specified dirty bit
 159      */
 160     private boolean isEffectDirty(EffectDirtyBits dirtyBit) {
 161         return ((effectDirty.get() & dirtyBit.getMask()) != 0);
 162     }
 163 
 164     /**
 165      * Clear the specified dirty bit
 166      */
 167     private void clearEffectDirty(EffectDirtyBits dirtyBit) {
 168         setEffectDirty(effectDirty.get() & ~dirtyBit.getMask());
 169     }
 170 
 171     final void sync() {
 172         if (isEffectDirty(EffectDirtyBits.EFFECT_DIRTY)) {
 173             update();
 174             clearEffectDirty(EffectDirtyBits.EFFECT_DIRTY);
 175         }
 176     }
 177 
 178     abstract void update();
 179 
 180     abstract boolean checkChainContains(Effect e);
 181 
 182     boolean containsCycles(Effect value) {
 183         if (value != null
 184                 && (value == this || value.checkChainContains(this))) {
 185             return true;
 186         }
 187         return false;
 188     }
 189 
 190     class EffectInputChangeListener extends EffectChangeListener {
 191         private int oldBits;
 192 
 193         public void register(Effect value) {
 194             super.register(value == null? null: value.effectDirtyProperty());
 195             if (value != null) {
 196                 oldBits = value.effectDirtyProperty().get();
 197             }
 198         }
 199 
 200         @Override
 201         public void invalidated(Observable valueModel) {
 202             int newBits = ((IntegerProperty)valueModel).get();
 203             int dirtyBits = newBits ^ oldBits;
 204             oldBits = newBits;
 205             if (EffectDirtyBits.isSet(dirtyBits, EffectDirtyBits.EFFECT_DIRTY)
 206                 && EffectDirtyBits.isSet(newBits, EffectDirtyBits.EFFECT_DIRTY)) {
 207                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 208             }
 209             if (EffectDirtyBits.isSet(dirtyBits, EffectDirtyBits.BOUNDS_CHANGED)) {
 210                 toggleDirty(EffectDirtyBits.BOUNDS_CHANGED);
 211             }
 212         }
 213     }
 214 
 215     class EffectInputProperty extends ObjectPropertyBase<Effect> {
 216         private final String propertyName;
 217 
 218         private Effect validInput = null;
 219 
 220         private final EffectInputChangeListener effectChangeListener =
 221                 new EffectInputChangeListener();
 222 
 223         public EffectInputProperty(final String propertyName) {
 224             this.propertyName = propertyName;
 225         }
 226 
 227         @Override
 228         public void invalidated() {
 229             final Effect newInput = super.get();
 230             if (containsCycles(newInput)) {
 231                 if (isBound()) {
 232                     unbind();
 233                     set(validInput);
 234                     throw new IllegalArgumentException("Cycle in effect chain "
 235                             + "detected, binding was set to incorrect value, "
 236                             + "unbinding the input property");
 237                 } else {
 238                     set(validInput);
 239                     throw new IllegalArgumentException("Cycle in effect chain detected");
 240                 }
 241             }
 242             validInput = newInput;
 243             effectChangeListener.register(newInput);
 244             markDirty(EffectDirtyBits.EFFECT_DIRTY);
 245 
 246             // we toggle dirty flag for bounds on this effect to notify
 247             // "consumers" of this effect that bounds have changed
 248             //
 249             // bounds of this effect might change
 250             // even if bounds of chained effect are not dirty
 251             effectBoundsChanged();
 252         }
 253 
 254         @Override
 255         public Object getBean() {
 256             return Effect.this;
 257         }
 258 
 259         @Override
 260         public String getName() {
 261             return propertyName;
 262         }
 263     }
 264 
 265    /**
 266     * Returns bounds of given node with applied effect.
 267     *
 268     * We *never* pass null in as a bounds. This method will
 269     * NOT take a null bounds object. The returned value may be
 270     * the same bounds object passed in, or it may be a new object.
 271     */
 272     abstract BaseBounds getBounds(BaseBounds bounds,
 273                                   BaseTransform tx,
 274                                   Node node,
 275                                   BoundsAccessor boundsAccessor);
 276 
 277     abstract Effect copy();
 278 
 279     static BaseBounds transformBounds(BaseTransform tx, BaseBounds r) {
 280         if (tx == null || tx.isIdentity()) {
 281             return r;
 282         }
 283         BaseBounds ret = new RectBounds();
 284         ret = tx.transform(r, ret);
 285         return ret;
 286     }
 287 
 288     // utility method used in calculation of bounds in BoxBlur and DropShadow effects
 289     static int getKernelSize(float fsize, int iterations) {
 290         int ksize = (int) Math.ceil(fsize);
 291         if (ksize < 1) ksize = 1;
 292         ksize = (ksize-1) * iterations + 1;
 293         ksize |= 1;
 294         return ksize / 2;
 295     }
 296 
 297     // utility method used for calculation of bounds in Shadow and DropShadow effects
 298     static BaseBounds getShadowBounds(BaseBounds bounds,
 299                                       BaseTransform tx,
 300                                       float width,
 301                                       float height,
 302                                       BlurType blurType) {
 303         int hgrow = 0;
 304         int vgrow = 0;
 305 
 306         switch (blurType) {
 307             case GAUSSIAN:
 308                 float hradius = width < 1.0f ? 0.0f : ((width - 1.0f) / 2.0f);
 309                 float vradius = height < 1.0f ? 0.0f : ((height - 1.0f) / 2.0f);
 310                 hgrow = (int) Math.ceil(hradius);
 311                 vgrow = (int) Math.ceil(vradius);
 312                 break;
 313             case ONE_PASS_BOX:
 314                 hgrow = getKernelSize(Math.round(width/3.0f), 1);
 315                 vgrow = getKernelSize(Math.round(height/3.0f), 1);
 316                 break;
 317             case TWO_PASS_BOX:
 318                 hgrow = getKernelSize(Math.round(width/3.0f), 2);
 319                 vgrow = getKernelSize(Math.round(height/3.0f), 2);
 320                 break;
 321             case THREE_PASS_BOX:
 322                 hgrow = getKernelSize(Math.round(width/3.0f), 3);
 323                 vgrow = getKernelSize(Math.round(height/3.0f), 3);
 324                 break;
 325         }
 326 
 327         bounds = bounds.deriveWithPadding(hgrow, vgrow, 0);
 328 
 329         return transformBounds(tx, bounds);
 330     }
 331 
 332     // Returns input bounds for an effect. These are either bounds of input effect or
 333     // geometric bounds of the node on which the effect calling this method is applied.
 334     static BaseBounds getInputBounds(BaseBounds bounds,
 335                                      BaseTransform tx,
 336                                      Node node,
 337                                      BoundsAccessor boundsAccessor,
 338                                      Effect input) {
 339         if (input != null) {
 340             bounds = input.getBounds(bounds, tx, node, boundsAccessor);
 341         } else {
 342             bounds = boundsAccessor.getGeomBounds(bounds, tx, node);
 343         }
 344 
 345         return bounds;
 346     }
 347 }