1 /*
   2  * Copyright (c) 2008, 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 com.sun.scenario.effect;
  27 
  28 import com.sun.javafx.geom.Point2D;
  29 import com.sun.javafx.geom.BaseBounds;
  30 import com.sun.javafx.geom.DirtyRegionContainer;
  31 import com.sun.javafx.geom.DirtyRegionPool;
  32 import com.sun.javafx.geom.Rectangle;
  33 import com.sun.javafx.geom.RectBounds;
  34 import com.sun.javafx.geom.transform.BaseTransform;
  35 import com.sun.scenario.effect.impl.state.RenderState;
  36 
  37 /**
  38  * An effect that shifts each pixel according to an (x,y) distance from
  39  * the (red,green) channels of a map image, respectively.
  40  */
  41 public class DisplacementMap extends CoreEffect {
  42 
  43     private FloatMap mapData;
  44     private float scaleX = 1f;
  45     private float scaleY = 1f;
  46     private float offsetX = 0f;
  47     private float offsetY = 0f;
  48     private boolean wrap;
  49 
  50     /**
  51      * Constructs a new {@code DisplacementMap} effect,
  52      * using the default input for source data.
  53      * This is a shorthand equivalent to:
  54      * <pre>
  55      *     new DisplacementMap(mapData, DefaultInput)
  56      * </pre>
  57      *
  58      * @throws IllegalArgumentException if {@code mapData} is null
  59      */
  60     public DisplacementMap(FloatMap mapData) {
  61         this(mapData, DefaultInput);
  62     }
  63 
  64     /**
  65      * Constructs a new {@code DisplacementMap} effect using the specified
  66      * input {@code Effect} for source data, or the default input if
  67      * {@code contentInput} is {@code null}.
  68      *
  69      * @param mapData the map data
  70      * @param contentInput the content input {@code Effect}
  71      * @throws IllegalArgumentException if {@code mapData} is null
  72      */
  73     public DisplacementMap(FloatMap mapData, Effect contentInput) {
  74         super(contentInput);
  75         setMapData(mapData);
  76         updatePeerKey("DisplacementMap");
  77     }
  78 
  79     /**
  80      * Returns the map data for this {@code Effect}.
  81      *
  82      * @return the map data for this {@code Effect}
  83      */
  84     public final FloatMap getMapData() {
  85         return mapData;
  86     }
  87 
  88     /**
  89      * Sets the map data for this {@code Effect}.
  90      *
  91      * @param mapData the map data for this {@code Effect}
  92      * @throws IllegalArgumentException if {@code mapData} is null
  93      */
  94     public void setMapData(FloatMap mapData) {
  95         if (mapData == null) {
  96             throw new IllegalArgumentException("Map data must be non-null");
  97         }
  98         FloatMap old = this.mapData;
  99         this.mapData = mapData;
 100     }
 101 
 102     /**
 103      * Returns the content input for this {@code Effect}.
 104      *
 105      * @return the content input for this {@code Effect}
 106      */
 107     public final Effect getContentInput() {
 108         return getInputs().get(0);
 109     }
 110 
 111     /**
 112      * Sets the content input for this {@code Effect} to a specific
 113      * {@code Effect} or to the default input if {@code input} is
 114      * {@code null}.
 115      *
 116      * @param contentInput the content input for this {@code Effect}
 117      */
 118     public void setContentInput(Effect contentInput) {
 119         setInput(0, contentInput);
 120     }
 121 
 122     /**
 123      * Returns the x scale factor.
 124      *
 125      * @return the x scale factor
 126      */
 127     public float getScaleX() {
 128         return scaleX;
 129     }
 130 
 131     /**
 132      * Sets the x scale factor.
 133      * <pre>
 134      *       Min: n/a
 135      *       Max: n/a
 136      *   Default: 1.0
 137      *  Identity: 1.0
 138      * </pre>
 139      *
 140      * @param scaleX the x scale factor
 141      */
 142     public void setScaleX(float scaleX) {
 143         float old = this.scaleX;
 144         this.scaleX = scaleX;
 145     }
 146 
 147     /**
 148      * Returns the y scale factor.
 149      *
 150      * @return the y scale factor
 151      */
 152     public float getScaleY() {
 153         return scaleY;
 154     }
 155 
 156     /**
 157      * Sets the y scale factor.
 158      * <pre>
 159      *       Min: n/a
 160      *       Max: n/a
 161      *   Default: 1.0
 162      *  Identity: 1.0
 163      * </pre>
 164      *
 165      * @param scaleY the y scale factor
 166      */
 167     public void setScaleY(float scaleY) {
 168         float old = this.scaleY;
 169         this.scaleY = scaleY;
 170     }
 171 
 172     /**
 173      * Returns the x offset factor.
 174      *
 175      * @return the x offset factor
 176      */
 177     public float getOffsetX() {
 178         return offsetX;
 179     }
 180 
 181     /**
 182      * Sets the x offset factor.
 183      * <pre>
 184      *       Min:  n/a
 185      *       Max:  n/a
 186      *   Default: -0.5
 187      *  Identity:  0.0
 188      * </pre>
 189      *
 190      * @param offsetX the x offset factor
 191      */
 192     public void setOffsetX(float offsetX) {
 193         float old = this.offsetX;
 194         this.offsetX = offsetX;
 195     }
 196 
 197     /**
 198      * Returns the y offset factor.
 199      *
 200      * @return the y offset factor
 201      */
 202     public float getOffsetY() {
 203         return offsetY;
 204     }
 205 
 206     /**
 207      * Sets the y offset factor.
 208      * <pre>
 209      *       Min:  n/a
 210      *       Max:  n/a
 211      *   Default: -0.5
 212      *  Identity:  0.0
 213      * </pre>
 214      *
 215      * @param offsetY the y offset factor
 216      */
 217     public void setOffsetY(float offsetY) {
 218         float old = this.offsetY;
 219         this.offsetY = offsetY;
 220     }
 221 
 222     /**
 223      * Returns whether values taken from outside the edges of the map
 224      * "wrap around" or not.
 225      *
 226      * @return true if wrapping is enabled, false otherwise
 227      */
 228     public boolean getWrap() {
 229         return this.wrap;
 230     }
 231 
 232     /**
 233      * Sets whether values taken from outside the edges of the map
 234      * "wrap around" or not.
 235      * <pre>
 236      *       Min:  n/a
 237      *       Max:  n/a
 238      *   Default: false
 239      *  Identity:  n/a
 240      * </pre>
 241      *
 242      * @param wrap true if wrapping is enabled, false otherwise
 243      */
 244     public void setWrap(boolean wrap) {
 245         boolean old = this.wrap;
 246         this.wrap = wrap;
 247     }
 248 
 249     /**
 250      * Transform the specified point {@code p} from the coordinate space
 251      * of the primary content input to the coordinate space of the effect
 252      * output.
 253      * In essence, this method asks the question "Which output coordinate
 254      * is most affected by the data at the specified coordinate in the
 255      * primary source input?"
 256      * <p>
 257      * Since the displacement map represents a mapping from output pixels
 258      * to relative source pixels, there may be multiple answers or no
 259      * answer for any given input coordinate so this method returns
 260      * {@code (NaN, NaN)}.
 261      *
 262      * @param p the point in the coordinate space of the primary content
 263      *          input to be transformed
 264      * @param defaultInput the default input {@code Effect} to be used in
 265      *                     all cases where a filter has a null input
 266      * @return the undefined point {@code (NaN, NaN)}
 267      */
 268     @Override
 269     public Point2D transform(Point2D p, Effect defaultInput) {
 270         return new Point2D(Float.NaN, Float.NaN);
 271     }
 272 
 273     /**
 274      * Transform the specified point {@code p} from the coordinate space
 275      * of the output of the effect into the coordinate space of the
 276      * primary content input.
 277      * In essence, this method asks the question "Which source coordinate
 278      * contributes most to the definition of the output at the specified
 279      * coordinate?"
 280      * <p>
 281      * This method returns the floating point coordinate where the
 282      * backwards mapping displacement map tells it to get the data for
 283      * the specified coordinate.
 284      *
 285      * @param p the point in the coordinate space of the result output
 286      *          to be transformed
 287      * @param defaultInput the default input {@code Effect} to be used in
 288      *                     all cases where a filter has a null input
 289      * @return the untransformed point in the coordinate space of the
 290      *         primary content input
 291      */
 292     @Override
 293     public Point2D untransform(Point2D p, Effect defaultInput) {
 294         BaseBounds r = getBounds(BaseTransform.IDENTITY_TRANSFORM, defaultInput);
 295         float rw = (float) r.getWidth();
 296         float rh = (float) r.getHeight();
 297         float x = (float) ((p.x - r.getMinX()) / rw);
 298         float y = (float) ((p.y - r.getMinY()) / rh);
 299         // If the coordinates are outside of the effect there is no
 300         // displacement effect occuring so we do not transform the point.
 301         if (x >= 0 && y >= 0 && x < 1 && y < 1) {
 302             int mx = (int) (x * mapData.getWidth());
 303             int my = (int) (y * mapData.getHeight());
 304             float dx = mapData.getSample(mx, my, 0);
 305             float dy = mapData.getSample(mx, my, 1);
 306             x += scaleX * (dx + offsetX);
 307             y += scaleY * (dy + offsetY);
 308             if (wrap) {
 309                 x -= Math.floor(x);
 310                 y -= Math.floor(y);
 311             }
 312             p = new Point2D(x * rw + r.getMinX(),
 313                             y * rh + r.getMinY());
 314         }
 315         return getDefaultedInput(0, defaultInput).untransform(p, defaultInput);
 316     }
 317 
 318     @Override
 319     public ImageData filterImageDatas(FilterContext fctx,
 320                                       BaseTransform transform,
 321                                       Rectangle outputClip,
 322                                       RenderState rstate,
 323                                       ImageData... inputs)
 324     {
 325         // NOTE: The floatmap is mapped to the entire output bounds so
 326         // we need to use unclipped math in the peers to get the
 327         // texture mapping right.
 328         return super.filterImageDatas(fctx, transform, null, rstate, inputs);
 329     }
 330     @Override
 331     public RenderState getRenderState(FilterContext fctx,
 332                                       BaseTransform transform,
 333                                       Rectangle outputClip,
 334                                       Object renderHelper,
 335                                       Effect defaultInput)
 336     {
 337         // NOTE: We could scan the map and calculate all possible input points
 338         // that might contribute to the clipped output?  Until then, any
 339         // pixel in the output could come from any pixel in the input so
 340         // we will need the full input bounds to proceed...
 341         return RenderState.UnclippedUserSpaceRenderState;
 342     }
 343 
 344     @Override
 345     public boolean reducesOpaquePixels() {
 346         return true;
 347     }
 348 
 349     @Override
 350     public DirtyRegionContainer getDirtyRegions(Effect defaultInput, DirtyRegionPool regionPool) {
 351         DirtyRegionContainer drc = regionPool.checkOut();
 352         drc.deriveWithNewRegion((RectBounds) getBounds(BaseTransform.IDENTITY_TRANSFORM, defaultInput));
 353         
 354         return drc;
 355     }
 356 }