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     }
 100 
 101     /**
 102      * Creates a new Effect.
 103      */
 104     protected Effect() {
 105        markDirty(EffectDirtyBits.EFFECT_DIRTY);
 106     }
 107 
 108     void effectBoundsChanged() {
 109         toggleDirty(EffectDirtyBits.BOUNDS_CHANGED);
 110     }
 111 
 112     private com.sun.scenario.effect.Effect peer;
 113     abstract com.sun.scenario.effect.Effect createPeer();
 114 
 115     com.sun.scenario.effect.Effect getPeer() {
 116         if (peer == null) {
 117             peer = createPeer();
 118         }
 119         return peer;
 120     }
 121 
 122     // effect is marked dirty in the constructor, so we don't need to be lazy here
 123     private IntegerProperty effectDirty =
 124             new SimpleIntegerProperty(this, "effectDirty");
 125 
 126     private void setEffectDirty(int value) {
 127         effectDirtyProperty().set(value);
 128     }
 129 
 130     private final IntegerProperty effectDirtyProperty() {
 131         return effectDirty;
 132     }
 133 
 134     private final boolean isEffectDirty() {
 135         return isEffectDirty(EffectDirtyBits.EFFECT_DIRTY);
 136     }
 137 
 138     /**
 139      * Set the specified dirty bit
 140      */
 141     final void markDirty(EffectDirtyBits dirtyBit) {
 142         setEffectDirty(effectDirty.get() | dirtyBit.getMask());
 143     }
 144 
 145     /**
 146      * Toggle the specified dirty bit
 147      */
 148     private void toggleDirty(EffectDirtyBits dirtyBit) {
 149         setEffectDirty(effectDirty.get() ^ dirtyBit.getMask());
 150     }
 151 
 152     /**
 153      * Test the specified dirty bit
 154      */
 155     private boolean isEffectDirty(EffectDirtyBits dirtyBit) {
 156         return ((effectDirty.get() & dirtyBit.getMask()) != 0);
 157     }
 158 
 159     /**
 160      * Clear the specified dirty bit
 161      */
 162     private void clearEffectDirty(EffectDirtyBits dirtyBit) {
 163         setEffectDirty(effectDirty.get() & ~dirtyBit.getMask());
 164     }
 165 
 166     final void sync() {
 167         if (isEffectDirty(EffectDirtyBits.EFFECT_DIRTY)) {
 168             update();
 169             clearEffectDirty(EffectDirtyBits.EFFECT_DIRTY);
 170         }
 171     }
 172 
 173     abstract void update();
 174 
 175     abstract boolean checkChainContains(Effect e);
 176 
 177     boolean containsCycles(Effect value) {
 178         if (value != null
 179                 && (value == this || value.checkChainContains(this))) {
 180             return true;
 181         }
 182         return false;
 183     }
 184 
 185     class EffectInputChangeListener extends EffectChangeListener {
 186         private int oldBits;
 187 
 188         public void register(Effect value) {
 189             super.register(value == null? null: value.effectDirtyProperty());
 190             if (value != null) {
 191                 oldBits = value.effectDirtyProperty().get();
 192             }
 193         }
 194 
 195         @Override
 196         public void invalidated(Observable valueModel) {
 197             int newBits = ((IntegerProperty)valueModel).get();
 198             int dirtyBits = newBits ^ oldBits;
 199             oldBits = newBits;
 200             if (EffectDirtyBits.isSet(dirtyBits, EffectDirtyBits.EFFECT_DIRTY)
 201                 && EffectDirtyBits.isSet(newBits, EffectDirtyBits.EFFECT_DIRTY)) {
 202                     markDirty(EffectDirtyBits.EFFECT_DIRTY);
 203             }
 204             if (EffectDirtyBits.isSet(dirtyBits, EffectDirtyBits.BOUNDS_CHANGED)) {
 205                 toggleDirty(EffectDirtyBits.BOUNDS_CHANGED);
 206             }
 207         }
 208     }
 209 
 210     class EffectInputProperty extends ObjectPropertyBase<Effect> {
 211         private final String propertyName;
 212 
 213         private Effect validInput = null;
 214 
 215         private final EffectInputChangeListener effectChangeListener =
 216                 new EffectInputChangeListener();
 217 
 218         public EffectInputProperty(final String propertyName) {
 219             this.propertyName = propertyName;
 220         }
 221 
 222         @Override
 223         public void invalidated() {
 224             final Effect newInput = super.get();
 225             if (containsCycles(newInput)) {
 226                 if (isBound()) {
 227                     unbind();
 228                     set(validInput);
 229                     throw new IllegalArgumentException("Cycle in effect chain "
 230                             + "detected, binding was set to incorrect value, "
 231                             + "unbinding the input property");
 232                 } else {
 233                     set(validInput);
 234                     throw new IllegalArgumentException("Cycle in effect chain detected");
 235                 }
 236             }
 237             validInput = newInput;
 238             effectChangeListener.register(newInput);
 239             markDirty(EffectDirtyBits.EFFECT_DIRTY);
 240 
 241             // we toggle dirty flag for bounds on this effect to notify
 242             // "consumers" of this effect that bounds have changed
 243             //
 244             // bounds of this effect might change
 245             // even if bounds of chained effect are not dirty
 246             effectBoundsChanged();
 247         }
 248 
 249         @Override
 250         public Object getBean() {
 251             return Effect.this;
 252         }
 253 
 254         @Override
 255         public String getName() {
 256             return propertyName;
 257         }
 258     }
 259 
 260    /**
 261     * Returns bounds of given node with applied effect.
 262     *
 263     * We *never* pass null in as a bounds. This method will
 264     * NOT take a null bounds object. The returned value may be
 265     * the same bounds object passed in, or it may be a new object.
 266     */
 267     abstract BaseBounds getBounds(BaseBounds bounds,
 268                                   BaseTransform tx,
 269                                   Node node,
 270                                   BoundsAccessor boundsAccessor);
 271 
 272     abstract Effect copy();
 273 
 274     static BaseBounds transformBounds(BaseTransform tx, BaseBounds r) {
 275         if (tx == null || tx.isIdentity()) {
 276             return r;
 277         }
 278         BaseBounds ret = new RectBounds();
 279         ret = tx.transform(r, ret);
 280         return ret;
 281     }
 282 
 283     // utility method used in calculation of bounds in BoxBlur and DropShadow effects
 284     static int getKernelSize(float fsize, int iterations) {
 285         int ksize = (int) Math.ceil(fsize);
 286         if (ksize < 1) ksize = 1;
 287         ksize = (ksize-1) * iterations + 1;
 288         ksize |= 1;
 289         return ksize / 2;
 290     }
 291 
 292     // utility method used for calculation of bounds in Shadow and DropShadow effects
 293     static BaseBounds getShadowBounds(BaseBounds bounds,
 294                                       BaseTransform tx,
 295                                       float width,
 296                                       float height,
 297                                       BlurType blurType) {
 298         int hgrow = 0;
 299         int vgrow = 0;
 300 
 301         switch (blurType) {
 302             case GAUSSIAN:
 303                 float hradius = width < 1.0f ? 0.0f : ((width - 1.0f) / 2.0f);
 304                 float vradius = height < 1.0f ? 0.0f : ((height - 1.0f) / 2.0f);
 305                 hgrow = (int) Math.ceil(hradius);
 306                 vgrow = (int) Math.ceil(vradius);
 307                 break;
 308             case ONE_PASS_BOX:
 309                 hgrow = getKernelSize(Math.round(width/3.0f), 1);
 310                 vgrow = getKernelSize(Math.round(height/3.0f), 1);
 311                 break;
 312             case TWO_PASS_BOX:
 313                 hgrow = getKernelSize(Math.round(width/3.0f), 2);
 314                 vgrow = getKernelSize(Math.round(height/3.0f), 2);
 315                 break;
 316             case THREE_PASS_BOX:
 317                 hgrow = getKernelSize(Math.round(width/3.0f), 3);
 318                 vgrow = getKernelSize(Math.round(height/3.0f), 3);
 319                 break;
 320         }
 321 
 322         bounds = bounds.deriveWithPadding(hgrow, vgrow, 0);
 323 
 324         return transformBounds(tx, bounds);
 325     }
 326 
 327     // Returns input bounds for an effect. These are either bounds of input effect or
 328     // geometric bounds of the node on which the effect calling this method is applied.
 329     static BaseBounds getInputBounds(BaseBounds bounds,
 330                                      BaseTransform tx,
 331                                      Node node,
 332                                      BoundsAccessor boundsAccessor,
 333                                      Effect input) {
 334         if (input != null) {
 335             bounds = input.getBounds(bounds, tx, node, boundsAccessor);
 336         } else {
 337             bounds = boundsAccessor.getGeomBounds(bounds, tx, node);
 338         }
 339 
 340         return bounds;
 341     }
 342 }