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.BooleanProperty; 30 import javafx.beans.property.BooleanPropertyBase; 31 import javafx.beans.property.DoubleProperty; 32 import javafx.beans.property.DoublePropertyBase; 33 import javafx.beans.property.ObjectProperty; 34 import javafx.beans.property.ObjectPropertyBase; 35 import javafx.scene.Node; 36 37 import com.sun.javafx.effect.EffectDirtyBits; 38 import com.sun.javafx.geom.BaseBounds; 39 import com.sun.javafx.geom.transform.BaseTransform; 40 import com.sun.javafx.scene.BoundsAccessor; 41 42 /** 43 * An effect that shifts each pixel by a distance specified by 44 * the first two bands of of the specified {@link FloatMap}. 45 * For each pixel in the output, the corresponding data from the 46 * {@code mapData} is retrieved, scaled and offset by the {@code scale} 47 * and {@code offset} attributes, scaled again by the size of the 48 * source input image and used as an offset from the destination pixel 49 * to retrieve the pixel data from the source input. 50 * <pre> 51 * dst[x,y] = src[(x,y) + (offset+scale*map[x,y])*(srcw,srch)] 52 * </pre> 53 * A value of {@code (0.0, 0.0)} would specify no offset for the 54 * pixel data whereas a value of {@code (0.5, 0.5)} would specify 55 * an offset of half of the source image size. 56 * <p> 57 * <b>Note</b> that the mapping is the offset from a destination pixel to 58 * the source pixel location from which it is sampled which means that 59 * filling the map with all values of {@code 0.5} would displace the 60 * image by half of its size towards the upper left since each destination 61 * pixel would contain the data that comes from the source pixel below and 62 * to the right of it. 63 * </p> 64 * <p> 65 * Also note that this effect does not adjust the coordinates of input 66 * events or any methods that measure containment on a {@code Node}. 67 * The results of mouse picking and the containment methods are undefined 68 * when a {@code Node} has a {@code DisplacementMap} effect in place. 69 * </p> 70 * <p> 71 * Example: 72 * <pre><code> 73 * int width = 220; 74 * int height = 100; 75 * 76 * FloatMap floatMap = new FloatMap(); 77 * floatMap.setWidth(width); 78 * floatMap.setHeight(height); 79 * 80 * for (int i = 0; i < width; i++) { 81 * double v = (Math.sin(i / 20.0 * Math.PI) - 0.5) / 40.0; 82 * for (int j = 0; j < height; j++) { 83 * floatMap.setSamples(i, j, 0.0f, (float) v); 84 * } 85 * } 86 * 87 * DisplacementMap displacementMap = new DisplacementMap(); 88 * displacementMap.setMapData(floatMap); 89 * 90 * Text text = new Text(); 91 * text.setX(40.0); 92 * text.setY(80.0); 93 * text.setText("Wavy Text"); 94 * text.setFill(Color.web("0x3b596d")); 95 * text.setFont(Font.font(null, FontWeight.BOLD, 50)); 96 * text.setEffect(displacementMap); 97 * 98 * </pre></code> 99 * 100 * <p> The code above produces the following: </p> 101 * <p> <img src="doc-files/displacementmap.png"/> </p> 102 * @since JavaFX 2.0 103 */ 104 public class DisplacementMap extends Effect { 105 @Override 106 com.sun.scenario.effect.DisplacementMap createPeer() { 107 return new com.sun.scenario.effect.DisplacementMap( 108 new com.sun.scenario.effect.FloatMap(1, 1), 109 com.sun.scenario.effect.Effect.DefaultInput); 110 }; 111 112 /** 113 * Creates a new instance of DisplacementMap with default parameters. 114 */ 115 public DisplacementMap() { 116 setMapData(new FloatMap(1, 1)); 117 } 118 119 /** 120 * Creates a new instance of DisplacementMap with the specified mapData. 121 * @param mapData the map data for this displacement map effect 122 * @since JavaFX 2.1 123 */ 124 public DisplacementMap(FloatMap mapData) { 125 setMapData(mapData); 126 } 127 128 /** 129 * Creates a new instance of DisplacementMap with the specified mapData, 130 * offsetX, offsetY, scaleX, and scaleY. 131 * @param mapData the map data for this displacement map effect 132 * @param offsetX the offset by which all x coordinate offset values in the 133 * {@code FloatMap} are displaced after they are scaled 134 * @param offsetY the offset by which all y coordinate offset values in the 135 * {@code FloatMap} are displaced after they are scaled 136 * @param scaleX the scale factor by which all x coordinate offset values in the 137 * {@code FloatMap} are multiplied 138 * @param scaleY the scale factor by which all y coordinate offset values in the 139 * {@code FloatMap} are multiplied 140 * @since JavaFX 2.1 141 */ 142 public DisplacementMap(FloatMap mapData, 143 double offsetX, double offsetY, 144 double scaleX, double scaleY) { 145 setMapData(mapData); 146 setOffsetX(offsetX); 147 setOffsetY(offsetY); 148 setScaleX(scaleX); 149 setScaleY(scaleY); 150 } 151 152 /** 153 * The input for this {@code Effect}. 154 * If set to {@code null}, or left unspecified, a graphical image of 155 * the {@code Node} to which the {@code Effect} is attached will be 156 * used as the input. 157 * @defaultValue null 158 */ 159 private ObjectProperty<Effect> input; 160 161 162 public final void setInput(Effect value) { 163 inputProperty().set(value); 164 } 165 166 public final Effect getInput() { 167 return input == null ? null : input.get(); 168 } 169 170 public final ObjectProperty<Effect> inputProperty() { 171 if (input == null) { 172 input = new EffectInputProperty("input"); 173 } 174 return input; 175 } 176 177 @Override 178 boolean checkChainContains(Effect e) { 179 Effect localInput = getInput(); 180 if (localInput == null) 181 return false; 182 if (localInput == e) 183 return true; 184 return localInput.checkChainContains(e); 185 } 186 187 private final FloatMap defaultMap = new FloatMap(1, 1); 188 189 /** 190 * The map data for this {@code Effect}. 191 * @defaultValue an empty map 192 */ 193 private ObjectProperty<FloatMap> mapData; 194 195 196 public final void setMapData(FloatMap value) { 197 mapDataProperty().set(value); 198 } 199 200 public final FloatMap getMapData() { 201 return mapData == null ? null : mapData.get(); 202 } 203 204 public final ObjectProperty<FloatMap> mapDataProperty() { 205 if (mapData == null) { 206 mapData = new ObjectPropertyBase<FloatMap>() { 207 208 @Override 209 public void invalidated() { 210 markDirty(EffectDirtyBits.EFFECT_DIRTY); 211 effectBoundsChanged(); 212 } 213 214 @Override 215 public Object getBean() { 216 return DisplacementMap.this; 217 } 218 219 @Override 220 public String getName() { 221 return "mapData"; 222 } 223 }; 224 } 225 return mapData; 226 } 227 228 private final MapDataChangeListener mapDataChangeListener = new MapDataChangeListener(); 229 230 private class MapDataChangeListener extends EffectChangeListener { 231 FloatMap mapData; 232 233 public void register(FloatMap value) { 234 mapData = value; 235 super.register(mapData == null ? null : mapData.effectDirtyProperty()); 236 } 237 238 @Override 239 public void invalidated(Observable valueModel) { 240 if (mapData.isEffectDirty()) { 241 markDirty(EffectDirtyBits.EFFECT_DIRTY); 242 effectBoundsChanged(); 243 } 244 } 245 }; 246 247 /** 248 * The scale factor by which all x coordinate offset values in the 249 * {@code FloatMap} are multiplied. 250 * <pre> 251 * Min: n/a 252 * Max: n/a 253 * Default: 1.0 254 * Identity: 1.0 255 * </pre> 256 * @defaultValue 1.0 257 */ 258 private DoubleProperty scaleX; 259 260 261 public final void setScaleX(double value) { 262 scaleXProperty().set(value); 263 } 264 265 public final double getScaleX() { 266 return scaleX == null ? 1 : scaleX.get(); 267 } 268 269 public final DoubleProperty scaleXProperty() { 270 if (scaleX == null) { 271 scaleX = new DoublePropertyBase(1) { 272 273 @Override 274 public void invalidated() { 275 markDirty(EffectDirtyBits.EFFECT_DIRTY); 276 } 277 278 @Override 279 public Object getBean() { 280 return DisplacementMap.this; 281 } 282 283 @Override 284 public String getName() { 285 return "scaleX"; 286 } 287 }; 288 } 289 return scaleX; 290 } 291 292 /** 293 * The scale factor by which all y coordinate offset values in the 294 * {@code FloatMap} are multiplied. 295 * <pre> 296 * Min: n/a 297 * Max: n/a 298 * Default: 1.0 299 * Identity: 1.0 300 * </pre> 301 * @defaultValue 1.0 302 */ 303 private DoubleProperty scaleY; 304 305 306 public final void setScaleY(double value) { 307 scaleYProperty().set(value); 308 } 309 310 public final double getScaleY() { 311 return scaleY == null ? 1 : scaleY.get(); 312 } 313 314 public final DoubleProperty scaleYProperty() { 315 if (scaleY == null) { 316 scaleY = new DoublePropertyBase(1) { 317 318 @Override 319 public void invalidated() { 320 markDirty(EffectDirtyBits.EFFECT_DIRTY); 321 } 322 323 @Override 324 public Object getBean() { 325 return DisplacementMap.this; 326 } 327 328 @Override 329 public String getName() { 330 return "scaleY"; 331 } 332 }; 333 } 334 return scaleY; 335 } 336 337 /** 338 * The offset by which all x coordinate offset values in the 339 * {@code FloatMap} are displaced after they are scaled. 340 * <pre> 341 * Min: n/a 342 * Max: n/a 343 * Default: 0.0 344 * Identity: 0.0 345 * </pre> 346 * @defaultValue 0.0 347 */ 348 private DoubleProperty offsetX; 349 350 351 public final void setOffsetX(double value) { 352 offsetXProperty().set(value); 353 } 354 355 public final double getOffsetX() { 356 return offsetX == null ? 0 : offsetX.get(); 357 } 358 359 public final DoubleProperty offsetXProperty() { 360 if (offsetX == null) { 361 offsetX = new DoublePropertyBase() { 362 363 @Override 364 public void invalidated() { 365 markDirty(EffectDirtyBits.EFFECT_DIRTY); 366 } 367 368 @Override 369 public Object getBean() { 370 return DisplacementMap.this; 371 } 372 373 @Override 374 public String getName() { 375 return "offsetX"; 376 } 377 }; 378 } 379 return offsetX; 380 } 381 382 /** 383 * The offset by which all y coordinate offset values in the 384 * {@code FloatMap} are displaced after they are scaled. 385 * <pre> 386 * Min: n/a 387 * Max: n/a 388 * Default: 0.0 389 * Identity: 0.0 390 * </pre> 391 * @defaultValue 0.0 392 */ 393 private DoubleProperty offsetY; 394 395 396 public final void setOffsetY(double value) { 397 offsetYProperty().set(value); 398 } 399 400 public final double getOffsetY() { 401 return offsetY == null ? 0 : offsetY.get(); 402 } 403 404 public final DoubleProperty offsetYProperty() { 405 if (offsetY == null) { 406 offsetY = new DoublePropertyBase() { 407 408 @Override 409 public void invalidated() { 410 markDirty(EffectDirtyBits.EFFECT_DIRTY); 411 } 412 413 @Override 414 public Object getBean() { 415 return DisplacementMap.this; 416 } 417 418 @Override 419 public String getName() { 420 return "offsetY"; 421 } 422 }; 423 } 424 return offsetY; 425 } 426 427 /** 428 * Defines whether values taken from outside the edges of the map 429 * "wrap around" or not. 430 * <pre> 431 * Min: n/a 432 * Max: n/a 433 * Default: false 434 * Identity: n/a 435 * </pre> 436 * @defaultValue false 437 */ 438 private BooleanProperty wrap; 439 440 441 public final void setWrap(boolean value) { 442 wrapProperty().set(value); 443 } 444 445 public final boolean isWrap() { 446 return wrap == null ? false : wrap.get(); 447 } 448 449 public final BooleanProperty wrapProperty() { 450 if (wrap == null) { 451 wrap = new BooleanPropertyBase() { 452 453 @Override 454 public void invalidated() { 455 markDirty(EffectDirtyBits.EFFECT_DIRTY); 456 } 457 458 @Override 459 public Object getBean() { 460 return DisplacementMap.this; 461 } 462 463 @Override 464 public String getName() { 465 return "wrap"; 466 } 467 }; 468 } 469 return wrap; 470 } 471 472 @Override 473 void update() { 474 Effect localInput = getInput(); 475 if (localInput != null) { 476 localInput.sync(); 477 } 478 479 com.sun.scenario.effect.DisplacementMap peer = 480 (com.sun.scenario.effect.DisplacementMap) getPeer(); 481 peer.setContentInput(localInput == null ? null : localInput.getPeer()); 482 FloatMap localMapData = getMapData(); 483 mapDataChangeListener.register(localMapData); 484 if (localMapData != null) { 485 localMapData.sync(); 486 peer.setMapData(localMapData.getImpl()); 487 } else { 488 defaultMap.sync(); 489 peer.setMapData(defaultMap.getImpl()); 490 } 491 peer.setScaleX((float)getScaleX()); 492 peer.setScaleY((float)getScaleY()); 493 peer.setOffsetX((float)getOffsetX()); 494 peer.setOffsetY((float)getOffsetY()); 495 peer.setWrap(isWrap()); 496 } 497 498 @Override 499 BaseBounds getBounds(BaseBounds bounds, 500 BaseTransform tx, 501 Node node, 502 BoundsAccessor boundsAccessor) { 503 bounds = getInputBounds(bounds, 504 BaseTransform.IDENTITY_TRANSFORM, 505 node, boundsAccessor, 506 getInput()); 507 return transformBounds(tx, bounds); 508 } 509 510 @Override 511 Effect copy() { 512 DisplacementMap dm = new DisplacementMap(this.getMapData().copy(), 513 this.getOffsetX(), this.getOffsetY(), this.getScaleX(), 514 this.getScaleY()); 515 dm.setInput(this.getInput()); 516 return dm; 517 } 518 }