javafx.scene.effect.DisplacementMap Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2022, 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 {@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.
*
* {@code dst[x, y] = src[(x, y) + (offset + scale * map[x, y]) * (srcw, srch)]}
*
* A value of {@code (0.0, 0.0)} would specify no offset for the
* pixel data whereas a value of {@code (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 {@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.
*
*
* 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.
*
*
* Example:
*
{@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);}
*
* The code above produces the following:
*
* @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.
*/
public DisplacementMap() {
setMapData(new FloatMap(1, 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.
* @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 {@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 input;
public final void setInput(Effect value) {
inputProperty().set(value);
}
public final Effect getInput() {
return input == null ? null : input.get();
}
public final ObjectProperty 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 {@code Effect}.
* @defaultValue an empty map
*/
private ObjectProperty mapData;
public final void setMapData(FloatMap value) {
mapDataProperty().set(value);
}
public final FloatMap getMapData() {
return mapData == null ? null : mapData.get();
}
public final ObjectProperty mapDataProperty() {
if (mapData == null) {
mapData = new ObjectPropertyBase<>() {
@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
* {@code FloatMap} are multiplied.
*
* Min: n/a
* Max: n/a
* Default: 1.0
* Identity: 1.0
*
* @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
* {@code FloatMap} are multiplied.
*
* Min: n/a
* Max: n/a
* Default: 1.0
* Identity: 1.0
*
* @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
* {@code FloatMap} are displaced after they are scaled.
*
* Min: n/a
* Max: n/a
* Default: 0.0
* Identity: 0.0
*
* @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
* {@code FloatMap} are displaced after they are scaled.
*
* Min: n/a
* Max: n/a
* Default: 0.0
* Identity: 0.0
*
* @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
*
* @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;
}
}