/*
 * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.effect;

import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.scene.Node;

import com.sun.javafx.effect.EffectDirtyBits;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.scene.BoundsAccessor;

An effect that shifts each pixel by a distance specified by the first two bands of of the specified FloatMap. For each pixel in the output, the corresponding data from the mapData is retrieved, scaled and offset by the scale and offset attributes, scaled again by the size of the source input image and used as an offset from the destination pixel to retrieve the pixel data from the source input.
    dst[x,y] = src[(x,y) + (offset+scale*map[x,y])*(srcw,srch)]
A value of (0.0, 0.0) would specify no offset for the pixel data whereas a value of (0.5, 0.5) would specify an offset of half of the source image size.

Note that the mapping is the offset from a destination pixel to the source pixel location from which it is sampled which means that filling the map with all values of 0.5 would displace the image by half of its size towards the upper left since each destination pixel would contain the data that comes from the source pixel below and to the right of it.

Also note that this effect does not adjust the coordinates of input events or any methods that measure containment on a Node. The results of mouse picking and the containment methods are undefined when a Node has a DisplacementMap effect in place.

Example:


int width = 220;
int height = 100;
FloatMap floatMap = new FloatMap();
floatMap.setWidth(width);
floatMap.setHeight(height);
for (int i = 0; i < width; i++) {
    double v = (Math.sin(i / 20.0 * Math.PI) - 0.5) / 40.0;
    for (int j = 0; j < height; j++) {
        floatMap.setSamples(i, j, 0.0f, (float) v);
    }
 }
DisplacementMap displacementMap = new DisplacementMap();
displacementMap.setMapData(floatMap);
Text text = new Text();
text.setX(40.0);
text.setY(80.0);
text.setText("Wavy Text");
text.setFill(Color.web("0x3b596d"));
text.setFont(Font.font(null, FontWeight.BOLD, 50));
text.setEffect(displacementMap);

The code above produces the following:

Since:JavaFX 2.0
/** * An effect that shifts each pixel by a distance specified by * the first two bands of of the specified {@link FloatMap}. * For each pixel in the output, the corresponding data from the * {@code mapData} is retrieved, scaled and offset by the {@code scale} * and {@code offset} attributes, scaled again by the size of the * source input image and used as an offset from the destination pixel * to retrieve the pixel data from the source input. * <pre> * dst[x,y] = src[(x,y) + (offset+scale*map[x,y])*(srcw,srch)] * </pre> * A value of {@code (0.0,&nbsp;0.0)} would specify no offset for the * pixel data whereas a value of {@code (0.5,&nbsp;0.5)} would specify * an offset of half of the source image size. * <p> * <b>Note</b> that the mapping is the offset from a destination pixel to * the source pixel location from which it is sampled which means that * filling the map with all values of {@code 0.5} would displace the * image by half of its size towards the upper left since each destination * pixel would contain the data that comes from the source pixel below and * to the right of it. * </p> * <p> * Also note that this effect does not adjust the coordinates of input * events or any methods that measure containment on a {@code Node}. * The results of mouse picking and the containment methods are undefined * when a {@code Node} has a {@code DisplacementMap} effect in place. * </p> * <p> * Example: * <pre>{@code * int width = 220; * int height = 100; * * FloatMap floatMap = new FloatMap(); * floatMap.setWidth(width); * floatMap.setHeight(height); * * for (int i = 0; i < width; i++) { * double v = (Math.sin(i / 20.0 * Math.PI) - 0.5) / 40.0; * for (int j = 0; j < height; j++) { * floatMap.setSamples(i, j, 0.0f, (float) v); * } * } * * DisplacementMap displacementMap = new DisplacementMap(); * displacementMap.setMapData(floatMap); * * Text text = new Text(); * text.setX(40.0); * text.setY(80.0); * text.setText("Wavy Text"); * text.setFill(Color.web("0x3b596d")); * text.setFont(Font.font(null, FontWeight.BOLD, 50)); * text.setEffect(displacementMap); * * }</pre> * * <p> The code above produces the following: </p> * <p> <img src="doc-files/displacementmap.png" alt="The visual effect of * DisplacementMap on text"> </p> * @since JavaFX 2.0 */
public class DisplacementMap extends Effect { @Override com.sun.scenario.effect.DisplacementMap createPeer() { return new com.sun.scenario.effect.DisplacementMap( new com.sun.scenario.effect.FloatMap(1, 1), com.sun.scenario.effect.Effect.DefaultInput); };
Creates a new instance of DisplacementMap with default parameters.
/** * Creates a new instance of DisplacementMap with default parameters. */
public DisplacementMap() { setMapData(new FloatMap(1, 1)); }
Creates a new instance of DisplacementMap with the specified mapData.
Params:
  • mapData – the map data for this displacement map effect
Since:JavaFX 2.1
/** * Creates a new instance of DisplacementMap with the specified mapData. * @param mapData the map data for this displacement map effect * @since JavaFX 2.1 */
public DisplacementMap(FloatMap mapData) { setMapData(mapData); }
Creates a new instance of DisplacementMap with the specified mapData, offsetX, offsetY, scaleX, and scaleY.
Params:
  • mapData – the map data for this displacement map effect
  • offsetX – the offset by which all x coordinate offset values in the FloatMap are displaced after they are scaled
  • offsetY – the offset by which all y coordinate offset values in the FloatMap are displaced after they are scaled
  • scaleX – the scale factor by which all x coordinate offset values in the FloatMap are multiplied
  • scaleY – the scale factor by which all y coordinate offset values in the FloatMap are multiplied
Since:JavaFX 2.1
/** * Creates a new instance of DisplacementMap with the specified mapData, * offsetX, offsetY, scaleX, and scaleY. * @param mapData the map data for this displacement map effect * @param offsetX the offset by which all x coordinate offset values in the * {@code FloatMap} are displaced after they are scaled * @param offsetY the offset by which all y coordinate offset values in the * {@code FloatMap} are displaced after they are scaled * @param scaleX the scale factor by which all x coordinate offset values in the * {@code FloatMap} are multiplied * @param scaleY the scale factor by which all y coordinate offset values in the * {@code FloatMap} are multiplied * @since JavaFX 2.1 */
public DisplacementMap(FloatMap mapData, double offsetX, double offsetY, double scaleX, double scaleY) { setMapData(mapData); setOffsetX(offsetX); setOffsetY(offsetY); setScaleX(scaleX); setScaleY(scaleY); }
The input for this Effect. If set to null, or left unspecified, a graphical image of the Node to which the Effect is attached will be used as the input.
@defaultValuenull
/** * The input for this {@code Effect}. * If set to {@code null}, or left unspecified, a graphical image of * the {@code Node} to which the {@code Effect} is attached will be * used as the input. * @defaultValue null */
private ObjectProperty<Effect> input; public final void setInput(Effect value) { inputProperty().set(value); } public final Effect getInput() { return input == null ? null : input.get(); } public final ObjectProperty<Effect> inputProperty() { if (input == null) { input = new EffectInputProperty("input"); } return input; } @Override boolean checkChainContains(Effect e) { Effect localInput = getInput(); if (localInput == null) return false; if (localInput == e) return true; return localInput.checkChainContains(e); } private final FloatMap defaultMap = new FloatMap(1, 1);
The map data for this Effect.
@defaultValuean empty map
/** * The map data for this {@code Effect}. * @defaultValue an empty map */
private ObjectProperty<FloatMap> mapData; public final void setMapData(FloatMap value) { mapDataProperty().set(value); } public final FloatMap getMapData() { return mapData == null ? null : mapData.get(); } public final ObjectProperty<FloatMap> mapDataProperty() { if (mapData == null) { mapData = new ObjectPropertyBase<FloatMap>() { @Override public void invalidated() { markDirty(EffectDirtyBits.EFFECT_DIRTY); effectBoundsChanged(); } @Override public Object getBean() { return DisplacementMap.this; } @Override public String getName() { return "mapData"; } }; } return mapData; } private final MapDataChangeListener mapDataChangeListener = new MapDataChangeListener(); private class MapDataChangeListener extends EffectChangeListener { FloatMap mapData; public void register(FloatMap value) { mapData = value; super.register(mapData == null ? null : mapData.effectDirtyProperty()); } @Override public void invalidated(Observable valueModel) { if (mapData.isEffectDirty()) { markDirty(EffectDirtyBits.EFFECT_DIRTY); effectBoundsChanged(); } } };
The scale factor by which all x coordinate offset values in the FloatMap are multiplied.
      Min: n/a
      Max: n/a
  Default: 1.0
 Identity: 1.0
@defaultValue1.0
/** * The scale factor by which all x coordinate offset values in the * {@code FloatMap} are multiplied. * <pre> * Min: n/a * Max: n/a * Default: 1.0 * Identity: 1.0 * </pre> * @defaultValue 1.0 */
private DoubleProperty scaleX; public final void setScaleX(double value) { scaleXProperty().set(value); } public final double getScaleX() { return scaleX == null ? 1 : scaleX.get(); } public final DoubleProperty scaleXProperty() { if (scaleX == null) { scaleX = new DoublePropertyBase(1) { @Override public void invalidated() { markDirty(EffectDirtyBits.EFFECT_DIRTY); } @Override public Object getBean() { return DisplacementMap.this; } @Override public String getName() { return "scaleX"; } }; } return scaleX; }
The scale factor by which all y coordinate offset values in the FloatMap are multiplied.
      Min: n/a
      Max: n/a
  Default: 1.0
 Identity: 1.0
@defaultValue1.0
/** * The scale factor by which all y coordinate offset values in the * {@code FloatMap} are multiplied. * <pre> * Min: n/a * Max: n/a * Default: 1.0 * Identity: 1.0 * </pre> * @defaultValue 1.0 */
private DoubleProperty scaleY; public final void setScaleY(double value) { scaleYProperty().set(value); } public final double getScaleY() { return scaleY == null ? 1 : scaleY.get(); } public final DoubleProperty scaleYProperty() { if (scaleY == null) { scaleY = new DoublePropertyBase(1) { @Override public void invalidated() { markDirty(EffectDirtyBits.EFFECT_DIRTY); } @Override public Object getBean() { return DisplacementMap.this; } @Override public String getName() { return "scaleY"; } }; } return scaleY; }
The offset by which all x coordinate offset values in the FloatMap are displaced after they are scaled.
      Min: n/a
      Max: n/a
  Default: 0.0
 Identity: 0.0
@defaultValue0.0
/** * The offset by which all x coordinate offset values in the * {@code FloatMap} are displaced after they are scaled. * <pre> * Min: n/a * Max: n/a * Default: 0.0 * Identity: 0.0 * </pre> * @defaultValue 0.0 */
private DoubleProperty offsetX; public final void setOffsetX(double value) { offsetXProperty().set(value); } public final double getOffsetX() { return offsetX == null ? 0 : offsetX.get(); } public final DoubleProperty offsetXProperty() { if (offsetX == null) { offsetX = new DoublePropertyBase() { @Override public void invalidated() { markDirty(EffectDirtyBits.EFFECT_DIRTY); } @Override public Object getBean() { return DisplacementMap.this; } @Override public String getName() { return "offsetX"; } }; } return offsetX; }
The offset by which all y coordinate offset values in the FloatMap are displaced after they are scaled.
      Min: n/a
      Max: n/a
  Default: 0.0
 Identity: 0.0
@defaultValue0.0
/** * The offset by which all y coordinate offset values in the * {@code FloatMap} are displaced after they are scaled. * <pre> * Min: n/a * Max: n/a * Default: 0.0 * Identity: 0.0 * </pre> * @defaultValue 0.0 */
private DoubleProperty offsetY; public final void setOffsetY(double value) { offsetYProperty().set(value); } public final double getOffsetY() { return offsetY == null ? 0 : offsetY.get(); } public final DoubleProperty offsetYProperty() { if (offsetY == null) { offsetY = new DoublePropertyBase() { @Override public void invalidated() { markDirty(EffectDirtyBits.EFFECT_DIRTY); } @Override public Object getBean() { return DisplacementMap.this; } @Override public String getName() { return "offsetY"; } }; } return offsetY; }
Defines whether values taken from outside the edges of the map "wrap around" or not.
      Min:  n/a
      Max:  n/a
  Default: false
 Identity:  n/a
@defaultValuefalse
/** * Defines whether values taken from outside the edges of the map * "wrap around" or not. * <pre> * Min: n/a * Max: n/a * Default: false * Identity: n/a * </pre> * @defaultValue false */
private BooleanProperty wrap; public final void setWrap(boolean value) { wrapProperty().set(value); } public final boolean isWrap() { return wrap == null ? false : wrap.get(); } public final BooleanProperty wrapProperty() { if (wrap == null) { wrap = new BooleanPropertyBase() { @Override public void invalidated() { markDirty(EffectDirtyBits.EFFECT_DIRTY); } @Override public Object getBean() { return DisplacementMap.this; } @Override public String getName() { return "wrap"; } }; } return wrap; } @Override void update() { Effect localInput = getInput(); if (localInput != null) { localInput.sync(); } com.sun.scenario.effect.DisplacementMap peer = (com.sun.scenario.effect.DisplacementMap) getPeer(); peer.setContentInput(localInput == null ? null : localInput.getPeer()); FloatMap localMapData = getMapData(); mapDataChangeListener.register(localMapData); if (localMapData != null) { localMapData.sync(); peer.setMapData(localMapData.getImpl()); } else { defaultMap.sync(); peer.setMapData(defaultMap.getImpl()); } peer.setScaleX((float)getScaleX()); peer.setScaleY((float)getScaleY()); peer.setOffsetX((float)getOffsetX()); peer.setOffsetY((float)getOffsetY()); peer.setWrap(isWrap()); } @Override BaseBounds getBounds(BaseBounds bounds, BaseTransform tx, Node node, BoundsAccessor boundsAccessor) { bounds = getInputBounds(bounds, BaseTransform.IDENTITY_TRANSFORM, node, boundsAccessor, getInput()); return transformBounds(tx, bounds); } @Override Effect copy() { DisplacementMap dm = new DisplacementMap(this.getMapData().copy(), this.getOffsetX(), this.getOffsetY(), this.getScaleX(), this.getScaleY()); dm.setInput(this.getInput()); return dm; } }