1 /* 2 * Copyright (c) 2012, 2014, Oracle and/or its affiliates. 3 * All rights reserved. Use is subject to license terms. 4 * 5 * This file is available and licensed under the following license: 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * - Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * - Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in 15 * the documentation and/or other materials provided with the distribution. 16 * - Neither the name of Oracle Corporation nor the names of its 17 * contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package ensemble.control; 33 34 35 import javafx.animation.AnimationTimer; 36 import javafx.beans.InvalidationListener; 37 import javafx.beans.Observable; 38 import javafx.beans.value.ChangeListener; 39 import javafx.beans.value.ObservableValue; 40 import javafx.geometry.Point2D; 41 import javafx.scene.Node; 42 import javafx.scene.Scene; 43 import javafx.scene.effect.DisplacementMap; 44 import javafx.scene.effect.FloatMap; 45 import javafx.scene.paint.*; 46 import javafx.scene.shape.*; 47 48 public class BookBend { 49 private Color bendEndColor = Color.LIGHTBLUE.interpolate(Color.BLACK, 0.6); 50 private Color bendStartColor = Color.LIGHTBLUE.darker(); 51 private Color pathColor = Color.LIGHTBLUE; 52 private int newWidth, newHeight; 53 private double oldTargetX, oldTargetY; 54 private double targetX = 250; 55 private double targetY = 250; 56 private Path clip; 57 private Path p; 58 private Path shadow; 59 private float[][] buffer = new float[1000][2]; 60 private FloatMap map = new FloatMap(); 61 private Node node; 62 private boolean updateNeeded = false; 63 private boolean isAnimationTimerActive = false; 64 private AnimationTimer animationTimer = new AnimationTimer() { 65 66 @Override 67 public void handle(long l) { 68 if(updateNeeded){ 69 update(); 70 } 71 } 72 }; 73 74 private void startAnimationTimer(){ 75 if(!isAnimationTimerActive){ 76 this.animationTimer.start(); 77 isAnimationTimerActive = true; 78 } 79 } 80 81 private void stopAnimationTimer(){ 82 this.animationTimer.stop(); 83 isAnimationTimerActive = false; 84 } 85 86 private void setUpdateNeeded(boolean value){ 87 if(isAnimationTimerActive == false && value == true && node.getScene() != null){ 88 startAnimationTimer(); 89 } 90 this.updateNeeded = value; 91 } 92 93 public BookBend(final Node node, Path path) { 94 this(node, path, null, null); 95 } 96 97 public BookBend(final Node node, Path path, Path shadow) { 98 this(node, path, shadow, null); 99 } 100 101 /** 102 * Creates DisplacementMap effect for the bend 103 * @param node target node 104 * @param path path that is used to draw the opposite side of a bend 105 * Its fill is updated with linear gradient. 106 * @param shadow path that is used to draw the shadow of a bend 107 * Its fill is updated with linear gradient. 108 * @param clip path that is used to clip the content of the page either 109 * for mouse operations or for visuals 110 */ 111 public BookBend(final Node node, Path path, Path shadow, Path clip) { 112 this.node = node; 113 this.p = path; 114 this.shadow = shadow; 115 this.clip = clip; 116 117 node.setEffect(new DisplacementMap(map)); 118 node.layoutBoundsProperty().addListener((Observable arg0) -> { 119 newWidth = (int) Math.round(node.getLayoutBounds().getWidth()); 120 newHeight = (int) Math.round(node.getLayoutBounds().getHeight()); 121 if (newWidth != map.getWidth() || newHeight != map.getHeight()) { 122 setUpdateNeeded(true); 123 } 124 }); 125 node.sceneProperty().addListener((ObservableValue<? extends Scene> ov, Scene oldValue, Scene newValue) -> { 126 if(newValue == null){ 127 stopAnimationTimer(); 128 } 129 }); 130 newWidth = (int) Math.round(node.getLayoutBounds().getWidth()); 131 newHeight = (int) Math.round(node.getLayoutBounds().getHeight()); 132 } 133 134 /** 135 * Sets colors for path gradient. Values are used on next update(). 136 * @param pathColor Path Color 137 * @param bendStartColor Starting path color 138 * @param bendEndColor Ending path color 139 */ 140 public void setColors(Color pathColor, Color bendStartColor, Color bendEndColor) { 141 this.pathColor = pathColor; 142 this.bendEndColor = bendEndColor; 143 this.bendStartColor = bendStartColor; 144 145 Paint fill = p.getFill(); 146 if (fill instanceof LinearGradient) { 147 LinearGradient lg = (LinearGradient) fill; 148 if (lg.getStops().size() >= 3) { 149 p.setFill(new LinearGradient(lg.getStartX(), lg.getStartY(), lg.getEndX(), lg.getEndY(), false, CycleMethod.NO_CYCLE, 150 new Stop(lg.getStops().get(0).getOffset(), pathColor), 151 new Stop(lg.getStops().get(1).getOffset(), bendStartColor), 152 new Stop(lg.getStops().get(2).getOffset(), bendEndColor))); 153 } 154 } 155 } 156 157 public double getTargetX() { 158 return targetX; 159 } 160 161 public double getTargetY() { 162 return targetY; 163 } 164 165 public Color getPathColor() { 166 return pathColor; 167 } 168 169 public Color getBendEndColor() { 170 return bendEndColor; 171 } 172 173 public Color getBendStartColor() { 174 return bendStartColor; 175 } 176 177 /** 178 * Updates DisplacementMap and path to target coordinates. 179 * @param targetX target X 180 * @param targetY target Y 181 */ 182 public void update(double targetX, double targetY) { 183 this.targetX = targetX; 184 this.targetY = targetY; 185 if (this.targetX != oldTargetX || this.targetY != oldTargetY) { 186 setUpdateNeeded(true); 187 } 188 } 189 190 /** 191 * Updates DisplacementMap and path for current coordinates. 192 */ 193 public void update() { 194 setUpdateNeeded(false); 195 196 if (newWidth == map.getWidth() && newHeight == map.getHeight() 197 && targetX == oldTargetX && targetY == oldTargetY) { 198 return; 199 } 200 oldTargetX = targetX; 201 oldTargetY = targetY; 202 if (newWidth != map.getWidth() || newHeight != map.getHeight()) { 203 map.setWidth(newWidth); 204 map.setHeight(newHeight); 205 } 206 207 final double W = node.getLayoutBounds().getWidth(); 208 final double H = node.getLayoutBounds().getHeight(); 209 210 // target point F for folded corner 211 final double xF = Math.min(targetX, W - 1); 212 final double yF = Math.min(targetY, H - 1); 213 214 final Point2D F = new Point2D(xF, yF); 215 216 // corner point O 217 final double xO = W; 218 final double yO = H; 219 220 // distance between them 221 final double FO = Math.hypot(xF - xO, yF - yO); 222 final double AF = FO / 2; 223 224 final double AC = Math.min(AF * 0.5, 200); 225 226 // radius of the fold as seen along the l2 line 227 final double R = AC / Math.PI * 1.5; 228 final double BC = R; 229 final double flat_R = AC; 230 231 // Gradient for the line from target point to corner point 232 final double K = (yO - yF) / (xO - xF); 233 234 // angle of a line l1 235 final double ANGLE = Math.atan(1 / K); 236 237 // point A (on line l1 - the mirror line of target and corner points) 238 final double xA = (xO + xF) / 2; 239 final double yA = (yO + yF) / 2; 240 241 // end points of line l1 242 final double bottomX = xA - (H - yA) * K; 243 final double bottomY = H; 244 final double rightX = W; 245 final double rightY = yA - (W - xA) / K; 246 247 final Point2D RL1 = new Point2D(rightX, rightY); 248 final Point2D BL1 = new Point2D(bottomX, bottomY); 249 250 // point C (on line l2 - the line when distortion begins) 251 final double kC = AC / AF; 252 final double xC = xA - (xA - xF) * kC; 253 final double yC = yA - (yA - yF) * kC; 254 255 final Point2D C = new Point2D(xC, yC); 256 257 final Point2D RL2 = new Point2D(W, yC - (W - xC) / K); 258 final Point2D BL2 = new Point2D(xC - (H - yC) * K, H); 259 260 // point B (on line l3 - the line where distortion ends) 261 final double kB = BC / AC; 262 final double xB = xC + (xA - xC) * kB; 263 final double yB = yC + (yA - yC) * kB; 264 265 // Bottom ellipse calculations 266 final Point2D BP1 = calcIntersection(F, BL1, BL2, C); 267 final Point2D BP3 = BL2; 268 final Point2D BP2 = middle(BP1, BP3, 0.5); 269 final Point2D BP4 = new Point2D(xB + BP2.getX() - xC, yB + BP2.getY() - yC); 270 271 final double bE_x1 = hypot(BP2, BP3); 272 final double bE_y2 = -hypot(BP2, BP4); 273 final double bE_yc = -hypot(BP2, BL1); 274 final double bE_y0 = bE_y2 * bE_y2 / (2 * bE_y2 - bE_yc); 275 final double bE_b = bE_y0 - bE_y2; 276 final double bE_a = Math.sqrt(-bE_x1 * bE_x1 / bE_y0 * bE_b * bE_b / bE_yc); 277 278 // Right ellipse calculations 279 final Point2D RP1 = calcIntersection(F, RL1, RL2, C); 280 final Point2D RP3 = RL2; 281 final Point2D RP2 = middle(RP1, RP3, 0.5); 282 final Point2D RP4 = new Point2D(xB + RP2.getX() - xC, yB + RP2.getY() - yC); 283 284 final double rE_x1 = hypot(RP2, RP3); 285 final double rE_y2 = -hypot(RP2, RP4); 286 final double rE_yc = -hypot(RP2, RL1); 287 final double rE_y0 = rE_y2 * rE_y2 / (2 * rE_y2 - rE_yc); 288 final double rE_b = rE_y0 - rE_y2; 289 final double rE_a = Math.sqrt(-rE_x1 * rE_x1 / rE_y0 * rE_b * rE_b / rE_yc); 290 291 p.setFill(new LinearGradient(xF, yF, xO, yO, false, CycleMethod.NO_CYCLE, 292 new Stop(0, pathColor), 293 new Stop((xC - xF) / (xO - xF), bendStartColor), 294 new Stop((xB - xF) / (xO - xF), bendEndColor))); 295 296 ArcTo arcTo1 = new ArcTo(); 297 arcTo1.setXAxisRotation(Math.toDegrees(-ANGLE)); 298 arcTo1.setRadiusX(bE_a); 299 arcTo1.setRadiusY(bE_b); 300 arcTo1.setX(BP1.getX()); 301 arcTo1.setY(BP1.getY()); 302 ArcTo arcTo2 = new ArcTo(); 303 arcTo2.setXAxisRotation(Math.toDegrees(-ANGLE)); 304 arcTo2.setRadiusX(rE_a); 305 arcTo2.setRadiusY(rE_b); 306 arcTo2.setX(RP4.getX()); 307 arcTo2.setY(RP4.getY()); 308 309 p.getElements().setAll( 310 new MoveTo(BP4.getX(), BP4.getY()), 311 arcTo1, 312 new LineTo(xF, yF), 313 new LineTo(RP1.getX(), RP1.getY()), 314 arcTo2, 315 new ClosePath()); 316 317 if (shadow != null) { 318 double level0 = (xB - xF) / (xO - xF) - R / FO * 0.5; 319 double level1 = (xB - xF) / (xO - xF) + (0.3 + (200 - AC) / 200) * R / FO; 320 shadow.setFill(new LinearGradient(xF, yF, xO, yO, false, CycleMethod.NO_CYCLE, 321 new Stop(level0, Color.rgb(0, 0, 0, 0.7)), 322 new Stop(level0 * 0.3 + level1 * 0.7, Color.rgb(0, 0, 0, 0.25)), 323 new Stop(level1, Color.rgb(0, 0, 0, 0.0)), 324 new Stop(1, Color.rgb(0, 0, 0, 0)))); 325 326 ArcTo arcTo3 = new ArcTo(); 327 arcTo3.setXAxisRotation(Math.toDegrees(-ANGLE)); 328 arcTo3.setRadiusX(rE_a); 329 arcTo3.setRadiusY(rE_b); 330 arcTo3.setX(RP4.getX()); 331 arcTo3.setY(RP4.getY()); 332 arcTo3.setSweepFlag(true); 333 ArcTo arcTo4 = new ArcTo(); 334 arcTo4.setXAxisRotation(Math.toDegrees(-ANGLE)); 335 arcTo4.setRadiusX(bE_a); 336 arcTo4.setRadiusY(bE_b); 337 arcTo4.setX(BP3.getX()); 338 arcTo4.setY(BP3.getY()); 339 arcTo4.setSweepFlag(true); 340 341 shadow.getElements().setAll( 342 new MoveTo(RP3.getX(), RP3.getY()), 343 arcTo3, 344 new LineTo(BP4.getX(), BP4.getY()), 345 arcTo4, 346 new LineTo(xO, yO), 347 new ClosePath()); 348 } 349 350 if (clip != null) { 351 final Point2D RL3 = new Point2D(W, yB - (W - xB) / K); 352 final Point2D BL3 = new Point2D(xB - (H - yB) * K, H); 353 354 clip.getElements().setAll( 355 new MoveTo(0, 0), 356 RL3.getY() > 0 ? new LineTo(W, 0) : new LineTo(0, 0), 357 RL3.getY() >= 0 ? new LineTo(RL3.getX(), RL3.getY()) : new LineTo(xB - (0 - yB) * K, 0), 358 BL3.getX() >= 0 ? new LineTo(BL3.getX(), BL3.getY()) : new LineTo(0, yB - (0 - xB) / K), 359 BL3.getX() > 0 ? new LineTo(0, H) : new LineTo(0, 0), 360 new ClosePath()); 361 } 362 363 final double K2 = -K; 364 final double C2 = BP3.getX() - K2 * H; 365 366 final double K3 = -K; 367 final double C3 = xB - K3 * yB; 368 369 final double STEP = Math.max(0.1, R / (buffer.length - 1)); 370 final double HYPOT = Math.hypot(1, K); 371 final double yR = 1.5 * R; 372 double x_1 = 0, y_1 = 0, cur_len = 0; 373 for (double len = 0; len <= R; len += STEP) { 374 final int index = (int) Math.round(len / STEP); 375 final double angle = Math.asin(len / R); 376 final double y = yR * Math.cos(angle); 377 if (len > 0) { 378 cur_len += Math.hypot(y - y_1, len - x_1); 379 } 380 buffer[index][0] = (float) angle; 381 buffer[index][1] = (float) (cur_len * flat_R); 382 x_1 = len; 383 y_1 = y; 384 } 385 double total_len = cur_len; 386 for (double len = 0; len <= R; len += STEP) { 387 final int index = (int) Math.round(len / STEP); 388 final double flat_len = buffer[index][1] / total_len; 389 final double delta_len = flat_len - len; 390 final double xs = delta_len / HYPOT; 391 final double ys = K * delta_len / HYPOT; 392 buffer[index][0] = (float) (xs / W); 393 buffer[index][1] = (float) (ys / H); 394 } 395 396 for (int y = 0; y < map.getHeight(); y++) { 397 final double lx2 = K2 * (y + 0.5) + C2; 398 final double lx3 = K3 * (y + 0.5) + C3; 399 for (int x = 0; x < map.getWidth(); x++) { 400 if (x + 0.5 < lx2) { 401 map.setSamples(x, y, 0, 0); 402 } else if (x + 0.5 >= lx3 - 1) { 403 map.setSamples(x, y, 1, 0); 404 } else { 405 final double len = Math.abs((x + 0.5) - K2 * (y + 0.5) - C2) / HYPOT; 406 final int index = (int) Math.round(len / STEP); 407 map.setSamples(x, y, buffer[index][0], buffer[index][1]); 408 } 409 } 410 } 411 } 412 413 private static Point2D calcIntersection(Point2D ap1, Point2D ap2, Point2D bp1, Point2D bp2) { 414 final double a1 = ap1.getY() - ap2.getY(); 415 final double b1 = ap2.getX() - ap1.getX(); 416 final double c1 = ap1.getX() * ap2.getY() - ap2.getX() * ap1.getY(); 417 final double a2 = bp1.getY() - bp2.getY(); 418 final double b2 = bp2.getX() - bp1.getX(); 419 final double c2 = bp1.getX() * bp2.getY() - bp2.getX() * bp1.getY(); 420 final double d = a1 * b2 - a2 * b1; 421 return new Point2D( 422 (b1 * c2 - b2 * c1) / d, 423 (c1 * a2 - c2 * a1) / d); 424 } 425 426 private static Point2D middle(Point2D a, Point2D a1, double value) { 427 return new Point2D( 428 a1.getX() * value + a.getX() * (1 - value), 429 a1.getY() * value + a.getY() * (1 - value)); 430 } 431 432 private static double hypot(Point2D p1, Point2D p2) { 433 return Math.hypot(p1.getX() - p2.getX(), p1.getY() - p2.getY()); 434 } 435 436 public void detach() { 437 node.setEffect(null); 438 } 439 }